from hermes_mcp.narrative_engine import STANCE_TAXONOMY, build_narrative from hermes_mcp.state_engine import synthesize_state def _bullish_state_payload(): concern = {"id": "c1", "account_id": "a1", "market_symbol": "BTCUSD", "status": "active"} regimes = [ {"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}}, {"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}}, {"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}}, {"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}}, {"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}}, {"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}}, ] state = synthesize_state(concern=concern, regimes=regimes) return concern, state.payload def test_build_narrative_produces_stable_taxonomy_output(): concern, payload = _bullish_state_payload() narrative = build_narrative(concern=concern, state_payload=payload) assert narrative.payload["stance"] in STANCE_TAXONOMY assert isinstance(narrative.key_drivers, list) assert isinstance(narrative.risk_flags, list) assert isinstance(narrative.uncertainties, list) assert 0.2 <= narrative.confidence <= 0.95 def test_build_narrative_describes_opportunity_without_deciding(): concern, payload = _bullish_state_payload() narrative = build_narrative(concern=concern, state_payload=payload) opportunity_map = narrative.payload["opportunity_map"] assert set(opportunity_map) == {"continuation", "mean_reversion", "reversal", "wait"} assert max(opportunity_map, key=opportunity_map.get) in {"continuation", "wait", "mean_reversion", "reversal"} assert "buy now" not in narrative.summary.lower() assert "sell now" not in narrative.summary.lower() def test_build_narrative_preserves_argus_context_as_context_only(): concern, payload = _bullish_state_payload() payload["argus_context"] = { "snapshot_regime": "risk_off", "snapshot_confidence": 0.72, "snapshot_summary": "macro stress remains elevated", "regime": "real_asset_pressure", "regime_confidence": 0.83, "regime_summary": "gold and silver confirm defensive cross-market pressure", } narrative = build_narrative(concern=concern, state_payload=payload) assert narrative.payload["argus_context"]["regime"] == "real_asset_pressure" assert any("argus context reads real_asset_pressure" in item for item in narrative.key_drivers) assert "buy now" not in narrative.summary.lower() assert "sell now" not in narrative.summary.lower()