Blum / backend /tests /test_learning_feedback_loop.py
Italianhype's picture
Deploy BLUM feedback loop certification
cec71a1 verified
Raw
History Blame Contribute Delete
13.6 kB
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