from datetime import date, timedelta from sqlalchemy import create_engine, select from sqlalchemy.orm import Session from app.core.database import Base from app.models import ( Asset, ExecutionSimulation, FeedbackLoopAudit, HistoricalPrediction, LearningFocusPriority, LearningRun, ModelVersion, PriceHistory, SignalPerformance, StrategyMemory, ) from app.services.learning_loop import ( BASE_SIGNAL_WEIGHTS, HistoricalSamplerService, LearningLoopService, ModelScoreService, PredictionEngine, learning_mode_metadata, ) from app.services.trading_game import TradingGameSimulator def setup_db() -> Session: engine = create_engine("sqlite:///:memory:", future=True) Base.metadata.create_all(engine) return Session(engine) def prediction_context() -> dict: return { "asset": {"ticker": "NVDA", "sector": "Technology"}, "analysis_date": "2024-06-01", "initial_price": 100.0, "technical": { "status": "ready", "trend_direction": "uptrend", "moving_averages": {"alignment": "bullish_stack"}, "technical_indicators": {"rsi": 62.0, "macd_hist": 1.0}, "volume": {"relative_volume": 1.2}, "volatility": {"regime": "medium"}, "levels": {"nearest_support": 94.0, "nearest_resistance": 118.0, "invalidation_level": 94.0}, "risk_reward_estimate": {"status": "estimated"}, "technical_summary": "Uptrend with constructive momentum.", }, "fundamentals": {"status": "ready", "quality_score": 56.0}, "news": {"article_count_total_as_of": 10, "article_count_14d": 8, "average_quality_14d": 2.0, "themes_14d": [{"theme": "AI"}]}, "macro": {"status": "ready"}, "market_context": {"market_regime": "Bull Expansion", "volatility_regime": "Low"}, "data_quality_score": 76.0, "point_in_time_policy": {"future": "hidden"}, } def seed_asset_history(db: Session) -> Asset: asset = Asset( ticker="NVDA", name="NVIDIA", category="Stock", sector="Technology", country="USA", asset_type="Stock", currency="USD", exchange="NASDAQ", is_active=True, ) db.add(asset) db.flush() start = date(2020, 1, 1) for offset in range(2200): close = 100.0 + offset * 0.05 db.add( PriceHistory( asset_id=asset.id, date=start + timedelta(days=offset), open=close - 0.5, high=close + 1.0, low=close - 1.0, close=close, volume=1_000_000 + offset * 100, provider="test", ) ) db.commit() return asset def test_active_model_version_weights_change_future_prediction_output(): with setup_db() as db: base = PredictionEngine().predict(prediction_context()) learned_weights = dict(BASE_SIGNAL_WEIGHTS) learned_weights["sentiment"] = 0.72 learned_weights["trend_structure"] = 0.04 db.add( ModelVersion( version="learned-sentiment-heavy", model_name="BLUM Learning Loop", weights=learned_weights, previous_weights=BASE_SIGNAL_WEIGHTS, is_active=True, ) ) db.commit() learned = PredictionEngine().predict(prediction_context(), db=db) assert learned["model_version_used"] == "learned-sentiment-heavy" assert learned["weights_used"]["sentiment"] > base["weights_used"]["sentiment"] assert learned["prediction"]["aggregate_score"] != base["prediction"]["aggregate_score"] def test_fallback_to_base_weights_without_active_model_version(): with setup_db() as db: prediction = PredictionEngine().predict(prediction_context(), db=db) assert prediction["model_version_used"] == "base-static" assert prediction["feedback_loop"]["weight_source"] == "base_signal_weights" assert prediction["weights_used"] == BASE_SIGNAL_WEIGHTS def test_learning_mode_metadata_is_mutually_exclusive(): training = learning_mode_metadata("test", {"sampling_reason": "learning_focus_priority"}) walk_forward = learning_mode_metadata("test", {"sampling_reason": "random_point_in_time"}) paper = learning_mode_metadata("paper_forward", {"mode": "paper_forward"}) assert training["mode"] == "training_replay" assert training["training_replay"] is True assert training["walk_forward_validation"] is False assert training["paper_forward"] is False assert walk_forward["mode"] == "walk_forward_validation" assert walk_forward["training_replay"] is False assert walk_forward["walk_forward_validation"] is True assert walk_forward["paper_forward"] is False assert paper["mode"] == "paper_forward" assert paper["training_replay"] is False assert paper["walk_forward_validation"] is False assert paper["paper_forward"] is True def test_signal_performance_changes_confidence(): with setup_db() as db: context = prediction_context() base = PredictionEngine().predict(context, db=db) db.add( SignalPerformance( signal_name="trend_structure", timeframe="mid", market_regime="Bull Expansion", sample_count=20, correct_count=16, false_positive_count=1, false_negative_count=0, reliability_score=78.0, weight_adjustment=0.05, ) ) db.commit() learned = PredictionEngine().predict(context, db=db) assert learned["feedback_loop"]["learning_memory_used"]["signal_performance"] assert learned["feedback_loop"]["strategy_memory_used"]["rows"] == [] assert learned["feedback_loop"]["confidence_adjustment"] > 0 assert learned["prediction"]["aggregate_confidence"] > base["prediction"]["aggregate_confidence"] def test_strategy_memory_changes_confidence(): with setup_db() as db: context = prediction_context() base = PredictionEngine().predict(context, db=db) db.add( StrategyMemory( memory_key="volume_confirmation:relative-volume", category="volume_confirmation", lesson="Momentum setups improve when volume confirms.", conditions={"relative_volume_gt": 1.0}, reliability_score=72.0, sample_count=8, positive_count=6, negative_count=2, ) ) db.commit() learned = PredictionEngine().predict(context, db=db) assert learned["feedback_loop"]["learning_memory_used"]["signal_performance"] == [] assert learned["feedback_loop"]["strategy_memory_used"]["rows"] assert learned["feedback_loop"]["confidence_adjustment"] > 0 assert learned["prediction"]["aggregate_confidence"] > base["prediction"]["aggregate_confidence"] def test_research_planner_priority_appears_in_sample_metadata(): with setup_db() as db: seed_asset_history(db) focus = LearningFocusPriority( priority_type="missed_entry_replay", target="NVDA", reason="Replay high-quality missed entries.", expected_learning_value=88.0, urgency="high", status="active", ) db.add(focus) db.commit() sample = HistoricalSamplerService().focus_priority_sample(db) assert sample is not None assert sample["sampling_reason"] == "learning_focus_priority" assert sample["learning_focus_priority_id"] == focus.id assert sample["priority_type"] == "missed_entry_replay" def test_run_single_sample_persists_weights_memory_research_priority_and_audit(): with setup_db() as db: asset = seed_asset_history(db) focus = LearningFocusPriority( priority_type="missed_entry_replay", target="NVDA", reason="Replay high-quality missed entries.", expected_learning_value=88.0, urgency="high", status="active", ) run = LearningRun(run_id="feedback-run", status="running", trigger="test") db.add_all([focus, run]) db.add( ModelVersion( version="learned-active", model_name="BLUM Learning Loop", weights={**BASE_SIGNAL_WEIGHTS, "momentum": 0.5}, previous_weights=BASE_SIGNAL_WEIGHTS, is_active=True, ) ) db.commit() report = LearningLoopService().run_single_sample( db, run, { "asset": asset, "analysis_date": date(2022, 5, 1), "sampling_reason": "learning_focus_priority", "learning_focus_priority_id": focus.id, "priority_type": focus.priority_type, }, ) prediction = db.scalar(select(HistoricalPrediction).where(HistoricalPrediction.ticker == "NVDA")) audit = db.scalar(select(FeedbackLoopAudit).where(FeedbackLoopAudit.prediction_id == prediction.id)) assert prediction.model_version_used == "learned-active" assert prediction.weights_used["momentum"] > BASE_SIGNAL_WEIGHTS["momentum"] assert prediction.learning_memory_used["policy"].startswith("SignalPerformance") assert prediction.strategy_memory_used["policy"].startswith("StrategyMemory") assert prediction.research_priority_used["priority_type"] == "missed_entry_replay" assert prediction.prediction_payload["learning_mode_metadata"]["training_replay"] is True assert prediction.prediction_payload["learning_mode_metadata"]["walk_forward_validation"] is False assert report["feedback_loop_audit"]["model_version_used"] == "learned-active" assert report["feedback_loop_audit"]["counterfactual_audit"]["baseline_prediction"]["model_version_used"] == "base-static" assert "score_delta" in report["feedback_loop_audit"]["counterfactual_audit"]["differences"] comparison = report["feedback_loop_audit"]["counterfactual_audit"]["outcome_comparison"] assert "baseline_direction_correct" in comparison assert "learned_direction_correct" in comparison assert "baseline_would_trade" in comparison assert "learned_would_trade" in comparison assert "avoided_loss" in comparison assert "missed_gain" in comparison assert "improvement_reason" in comparison assert report["feedback_loop_audit"]["improvement_detected"] == comparison["improvement_detected"] assert audit is not None assert audit.future_decision_json["ticker"] == "NVDA" assert audit.changes_applied_json["counterfactual_audit"]["learned_prediction"]["model_version_used"] == "learned-active" assert audit.outcome_json["improvement_reason"] == comparison["improvement_reason"] def test_paper_trade_payload_includes_feedback_loop_metadata(): with setup_db() as db: asset = seed_asset_history(db) run = LearningRun(run_id="feedback-trade-run", status="running", trigger="test") db.add(run) db.add( ModelVersion( version="learned-paper", model_name="BLUM Learning Loop", weights={**BASE_SIGNAL_WEIGHTS, "momentum": 0.5}, previous_weights=BASE_SIGNAL_WEIGHTS, is_active=True, ) ) db.commit() LearningLoopService().run_single_sample(db, run, {"asset": asset, "analysis_date": date(2022, 5, 1)}) prediction = db.scalar(select(HistoricalPrediction).where(HistoricalPrediction.ticker == "NVDA")) simulation = ExecutionSimulation( prediction_id=prediction.id, ticker="NVDA", setup_type="momentum_breakout", realized_r_multiple=1.4, max_adverse_excursion=-0.4, max_favorable_excursion=1.8, time_in_trade=12, target_hit=True, simulation_payload={"timeframe": "daily"}, ) db.add(simulation) simulator = TradingGameSimulator() game = simulator.create_game(db, reason="test") db.flush() trade = simulator.apply_simulation(db, game, simulation, prediction) feedback = trade.payload["feedback_loop"] assert feedback["model_version_used"] == "learned-paper" assert feedback["weights_used"]["momentum"] > BASE_SIGNAL_WEIGHTS["momentum"] assert feedback["learning_memory_used"]["policy"].startswith("SignalPerformance") assert feedback["strategy_memory_used"]["policy"].startswith("StrategyMemory") assert "learning_mode_metadata" in feedback def test_model_version_is_not_created_with_insufficient_evidence(): with setup_db() as db: db.add( SignalPerformance( signal_name="momentum", timeframe="short", market_regime="Bull Expansion", sample_count=1, correct_count=1, false_positive_count=0, false_negative_count=0, reliability_score=85.0, weight_adjustment=0.1, ) ) db.commit() result = ModelScoreService().recalculate(db) assert result["status"] == "insufficient_evidence" assert result["thresholds"]["min_outcomes"] >= 30 assert db.scalar(select(ModelVersion)) is None