test_narrative_engine.py 4.0 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253
  1. from hermes_mcp.narrative_engine import STANCE_TAXONOMY, build_narrative
  2. from hermes_mcp.state_engine import synthesize_state
  3. def _bullish_state_payload():
  4. concern = {"id": "c1", "account_id": "a1", "market_symbol": "BTCUSD", "status": "active"}
  5. regimes = [
  6. {"timeframe": "1m", "price": 101, "trend": {"ema_fast": 100, "ema_slow": 99, "sma_long": 98}, "momentum": {"rsi": 62, "macd_histogram": 0.02}, "volatility": {"atr_percent": 0.5}, "bands": {"bollinger": {"middle": 100, "upper": 102, "lower": 98}}, "vwap": 100.2, "reversal": {"direction": "none", "score": 0}},
  7. {"timeframe": "5m", "price": 102, "trend": {"ema_fast": 101, "ema_slow": 100, "sma_long": 99}, "momentum": {"rsi": 63, "macd_histogram": 0.018}, "volatility": {"atr_percent": 0.6}, "bands": {"bollinger": {"middle": 101, "upper": 103, "lower": 99}}, "vwap": 101.1, "reversal": {"direction": "none", "score": 0}},
  8. {"timeframe": "15m", "price": 103, "trend": {"ema_fast": 102, "ema_slow": 101, "sma_long": 99.5}, "momentum": {"rsi": 60, "macd_histogram": 0.012}, "volatility": {"atr_percent": 0.8}, "bands": {"bollinger": {"middle": 102, "upper": 104, "lower": 100}}, "vwap": 102.0, "reversal": {"direction": "none", "score": 0}},
  9. {"timeframe": "1h", "price": 104, "trend": {"ema_fast": 103, "ema_slow": 102, "sma_long": 100}, "momentum": {"rsi": 58, "macd_histogram": 0.01}, "volatility": {"atr_percent": 0.9}, "bands": {"bollinger": {"middle": 103, "upper": 105, "lower": 101}}, "vwap": 103.0, "reversal": {"direction": "none", "score": 0}},
  10. {"timeframe": "4h", "price": 106, "trend": {"ema_fast": 104, "ema_slow": 103, "sma_long": 101}, "momentum": {"rsi": 57, "macd_histogram": 0.009}, "volatility": {"atr_percent": 1.1}, "bands": {"bollinger": {"middle": 104, "upper": 107, "lower": 101}}, "vwap": 104.5, "reversal": {"direction": "none", "score": 0}},
  11. {"timeframe": "1d", "price": 110, "trend": {"ema_fast": 108, "ema_slow": 106, "sma_long": 103}, "momentum": {"rsi": 61, "macd_histogram": 0.008}, "volatility": {"atr_percent": 1.4}, "bands": {"bollinger": {"middle": 107, "upper": 112, "lower": 102}}, "vwap": 108.0, "reversal": {"direction": "none", "score": 0}},
  12. ]
  13. state = synthesize_state(concern=concern, regimes=regimes)
  14. return concern, state.payload
  15. def test_build_narrative_produces_stable_taxonomy_output():
  16. concern, payload = _bullish_state_payload()
  17. narrative = build_narrative(concern=concern, state_payload=payload)
  18. assert narrative.payload["stance"] in STANCE_TAXONOMY
  19. assert isinstance(narrative.key_drivers, list)
  20. assert isinstance(narrative.risk_flags, list)
  21. assert isinstance(narrative.uncertainties, list)
  22. assert 0.2 <= narrative.confidence <= 0.95
  23. def test_build_narrative_describes_opportunity_without_deciding():
  24. concern, payload = _bullish_state_payload()
  25. narrative = build_narrative(concern=concern, state_payload=payload)
  26. opportunity_map = narrative.payload["opportunity_map"]
  27. assert set(opportunity_map) == {"continuation", "mean_reversion", "reversal", "wait"}
  28. assert max(opportunity_map, key=opportunity_map.get) in {"continuation", "wait", "mean_reversion", "reversal"}
  29. assert "buy now" not in narrative.summary.lower()
  30. assert "sell now" not in narrative.summary.lower()
  31. def test_build_narrative_preserves_argus_context_as_context_only():
  32. concern, payload = _bullish_state_payload()
  33. payload["argus_context"] = {
  34. "snapshot_regime": "risk_off",
  35. "snapshot_confidence": 0.72,
  36. "snapshot_summary": "macro stress remains elevated",
  37. "regime": "real_asset_pressure",
  38. "regime_confidence": 0.83,
  39. "regime_summary": "gold and silver confirm defensive cross-market pressure",
  40. }
  41. narrative = build_narrative(concern=concern, state_payload=payload)
  42. assert narrative.payload["argus_context"]["regime"] == "real_asset_pressure"
  43. assert any("argus context reads real_asset_pressure" in item for item in narrative.key_drivers)
  44. assert "buy now" not in narrative.summary.lower()
  45. assert "sell now" not in narrative.summary.lower()