"""Ollama-Anbindung: Antwortverarbeitung, Fehlerdiagnose, Prompt-Aufbau.""" from __future__ import annotations import asyncio import pytest from aiohttp import web from trademind.config import OllamaConfig from trademind.llm import SYSTEM_PROMPT, OllamaClient, build_status_prompt STATUS = { "mode": "paper", "exchange": "binance", "symbols": ["BTC/USDT", "ETH/USDT"], "timeframe": "5m", "quote_currency": "USDT", "portfolio": {"equity": 10123.45, "total_return_pct": 1.23, "trades": 7, "win_rate": 0.42, "profit_factor": 1.1, "max_drawdown_pct": 2.5, "open_positions": 1}, "strategy": {"candidates_seen": 40, "candidates_accepted": 9, "learner": {"samples_seen": 900, "trade_samples": 7, "online_accuracy": 0.53}}, "risk": {"halted": False, "halt_reason": "", "max_open_positions": 3, "max_position_pct": 0.2}, "trading": {"active": True, "simulated": True}, "feature_weights": {"ema_spread": 0.39, "trend_dist": -0.29, "rsi_norm": 0.01}, "positions": [{"symbol": "BTC/USDT", "entry_price": 70000.0, "mark_price": 70500.0, "unrealized_pct": 0.71, "bars_held": 4, "confidence": 0.62}], "recent_trades": [{"symbol": "ETH/USDT", "pnl_pct": -0.004, "exit_reason": "stop_loss", "bars_held": 9}], } # ------------------------------------------------------------------- Prompt def test_prompt_contains_the_real_numbers(): prompt = build_status_prompt(STATUS) for needle in ("paper", "binance", "BTC/USDT", "10123.45", "ema_spread", "stop_loss"): assert needle in prompt, f"{needle} fehlt im Prompt" def test_prompt_ranks_weights_by_magnitude(): prompt = build_status_prompt(STATUS, top_weights=2) assert "ema_spread" in prompt and "trend_dist" in prompt assert "rsi_norm" not in prompt, "das schwächste Gewicht sollte wegfallen" def test_prompt_survives_a_bare_status(): prompt = build_status_prompt({}) assert "PORTFOLIO" in prompt and "LERNMODELL" in prompt def test_system_prompt_forbids_advice(): for needle in ("keine Anlageempfehlung", "keine Kursprognose", "Erfinde keine Zahlen"): assert needle in SYSTEM_PROMPT # ------------------------------------------------------------ Falsches Ollama def fake_ollama(handler): app = web.Application() app.router.add_post("/api/generate", handler) app.router.add_get("/api/tags", lambda _: web.json_response({"models": [{"name": "testmodell"}]})) return app @pytest.fixture async def client_for(aiohttp_server): async def _make(handler, **overrides) -> OllamaClient: server = await aiohttp_server(fake_ollama(handler)) options = {"model": "testmodell", "timeout_seconds": 5, **overrides} config = OllamaConfig( enabled=True, base_url=str(server.make_url("/")).rstrip("/"), **options ) return OllamaClient(config) return _make async def test_plain_answer_is_returned(client_for): async def handler(request): assert (await request.json())["stream"] is False return web.json_response({"response": "Alles ruhig.", "done_reason": "stop"}) client = await client_for(handler) result = await client.generate("frage") assert result.ok and result.text == "Alles ruhig." assert result.model == "testmodell" await client.close() async def test_thinking_block_is_stripped(client_for): async def handler(_): return web.json_response( {"response": "erst überlegenDas Ergebnis.", "done_reason": "stop"} ) client = await client_for(handler) result = await client.generate("frage") assert result.ok and result.text == "Das Ergebnis." await client.close() async def test_reasoning_model_without_answer_is_diagnosed(client_for): """Der reale Fall: qwen3 verbraucht das Token-Budget für 'thinking'.""" async def handler(_): return web.json_response({"response": "", "thinking": "x" * 1800, "done_reason": "length"}) client = await client_for(handler) result = await client.generate("frage") assert not result.ok assert "Denkschritte" in result.error assert "llm.think" in result.error, "die Meldung muss den Ausweg nennen" await client.close() async def test_truncated_answer_is_diagnosed(client_for): async def handler(_): return web.json_response({"response": "", "done_reason": "length"}) client = await client_for(handler) result = await client.generate("frage") assert not result.ok and "max_tokens" in result.error await client.close() async def test_think_flag_is_sent(client_for): seen = {} async def handler(request): seen.update(await request.json()) return web.json_response({"response": "ok", "done_reason": "stop"}) client = await client_for(handler) await client.generate("frage") assert seen["think"] is False, "Reasoning ist standardmäßig aus" await client.close() async def test_old_ollama_without_think_field_still_works(client_for): """Ältere Versionen lehnen das Feld ab – dann ohne es erneut versuchen.""" calls = [] async def handler(request): body = await request.json() calls.append("think" in body) if "think" in body: return web.json_response({"error": "unknown field think"}, status=400) return web.json_response({"response": "Klappt doch.", "done_reason": "stop"}) client = await client_for(handler) result = await client.generate("frage") assert result.ok and result.text == "Klappt doch." assert calls == [True, False] await client.close() async def test_http_error_is_reported(client_for): async def handler(_): return web.json_response({"error": "model not found"}, status=404) client = await client_for(handler) result = await client.generate("frage") assert not result.ok and "404" in result.error await client.close() async def test_timeout_is_reported_with_a_hint(client_for): async def handler(_): await asyncio.sleep(2) return web.json_response({"response": "zu spät"}) client = await client_for(handler, timeout_seconds=0.2) result = await client.generate("frage") assert not result.ok assert "Zeitüberschreitung" in result.error and "timeout_seconds" in result.error await client.close() async def test_unreachable_server_is_reported(aiohttp_server): """Server starten, wieder beenden, dann anfragen – die Verbindung wird abgelehnt.""" async def handler(_): return web.json_response({"response": "nie erreicht"}) server = await aiohttp_server(fake_ollama(handler)) url = str(server.make_url("/")).rstrip("/") await server.close() client = OllamaClient(OllamaConfig(enabled=True, base_url=url, timeout_seconds=5)) result = await client.generate("frage") assert not result.ok assert "nicht erreichbar" in result.error and url in result.error await client.close() async def test_answer_is_capped(client_for): async def handler(_): return web.json_response({"response": "y" * 5000, "done_reason": "stop"}) client = await client_for(handler, max_answer_chars=100) result = await client.generate("frage") assert result.ok and len(result.text) == 100 await client.close() async def test_model_listing(client_for): async def handler(_): return web.json_response({"response": "ok"}) client = await client_for(handler) assert await client.available_models() == ["testmodell"] await client.close() def test_llm_is_off_by_default(): from trademind.config import Config assert Config().llm.enabled is False