From ef6e2f160c3d1302c5aa95ee3e250f6066712d5e Mon Sep 17 00:00:00 2001 From: drjones Date: Sat, 28 Feb 2026 18:57:34 -0800 Subject: [PATCH] Add robust Ollama output normalizer with fallback parse path for decision reliability --- services.py | 72 ++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 58 insertions(+), 14 deletions(-) diff --git a/services.py b/services.py index a621315..a77e490 100644 --- a/services.py +++ b/services.py @@ -89,11 +89,13 @@ def extra_research(symbol: str, weak_points: list, limit: int = 6): def summarize_news_with_ollama(symbol: str, context_items: list): prompt = {"task": "Summarize market-moving info into a concise brief", "symbol": symbol, "news": context_items} + fallback = " | ".join([(x.get("title") or "")[:90] for x in context_items[:3] if x.get("title")]) or f"No strong headlines for {symbol}" try: parsed = _ollama_generate(settings.ollama_curator_model, prompt) - return parsed.get("summary", "no-summary") + s = parsed.get("summary") + return s if s else fallback except Exception: - return "summary-unavailable" + return fallback def strategy_signals(symbol: str, context_items: list): @@ -106,6 +108,30 @@ def strategy_signals(symbol: str, context_items: list): return {"strategy": "event-momentum-v1", "score": score, "action": action, "confidence": conf, "signals": {"bullish": bullish, "bearish": bearish}} +def _normalize_decision(d: dict, strategy: dict): + action = str(d.get("action", strategy.get("action", "hold"))).lower() + if action not in {"buy", "sell", "hold"}: + action = strategy.get("action", "hold") + try: + confidence = max(0.0, min(1.0, float(d.get("confidence", strategy.get("confidence", 0.5))))) + except Exception: + confidence = min(strategy.get("confidence", 0.5), 0.55) + try: + order_usd = float(d.get("order_usd", settings.max_order_usd)) + except Exception: + order_usd = settings.max_order_usd + order_usd = max(1.0, min(order_usd, settings.max_order_usd)) + reason = str(d.get("reason", "normalized-decision"))[:500] + return { + "action": action, + "confidence": confidence, + "order_usd": order_usd, + "reason": reason, + "needs_more_research": bool(d.get("needs_more_research", False)), + "research_topics": d.get("research_topics", []) or [], + } + + def llm_final_decision(symbol: str, context_items: list, strategy: dict, memory_hits: list): prompt = { "task": "Final trading decision. Return strict JSON.", @@ -116,21 +142,39 @@ def llm_final_decision(symbol: str, context_items: list, strategy: dict, memory_ "news": context_items, "output_schema": {"action": "buy|sell|hold", "confidence": "0-1", "order_usd": f"<= {settings.max_order_usd}", "reason": "short rationale", "needs_more_research": True, "research_topics": ["..."]}, } + # pass 1: strict json mode try: d = _ollama_generate(settings.ollama_decision_model, prompt, timeout=60) - action = str(d.get("action", "hold")).lower() - if action not in {"buy", "sell", "hold"}: - action = "hold" - return { - "action": action, - "confidence": max(0.0, min(1.0, float(d.get("confidence", 0.5)))), - "order_usd": min(float(d.get("order_usd", settings.max_order_usd)), settings.max_order_usd), - "reason": d.get("reason", "fallback"), - "needs_more_research": bool(d.get("needs_more_research", False)), - "research_topics": d.get("research_topics", []) or [], - } + return _normalize_decision(d, strategy) except Exception: - return {"action": strategy.get("action", "hold"), "confidence": min(strategy.get("confidence", 0.5), 0.55), "order_usd": min(1.0, settings.max_order_usd), "reason": "decision-fallback-strategy", "needs_more_research": False, "research_topics": []} + pass + + # pass 2: non-json constrained output, then parse heuristically + try: + text_prompt = ( + f"Symbol: {symbol}\n" + f"Strategy prior: {strategy}\n" + f"Return 4 lines only:\n" + f"action: buy|sell|hold\nconfidence: 0-1\norder_usd: <= {settings.max_order_usd}\nreason: \n" + ) + r = requests.post(f"{settings.ollama_url}/api/generate", json={"model": settings.ollama_decision_model, "prompt": text_prompt, "stream": False}, timeout=45) + if r.ok: + raw = (r.json().get("response", "") or "").lower() + action = "buy" if "buy" in raw else ("sell" if "sell" in raw else "hold") + conf = 0.6 if "confidence" not in raw else strategy.get("confidence", 0.55) + parsed = {"action": action, "confidence": conf, "order_usd": settings.max_order_usd, "reason": raw[:300]} + return _normalize_decision(parsed, strategy) + except Exception: + pass + + return { + "action": strategy.get("action", "hold"), + "confidence": min(strategy.get("confidence", 0.5), 0.55), + "order_usd": min(5.0, settings.max_order_usd), + "reason": "decision-fallback-strategy", + "needs_more_research": False, + "research_topics": [], + } def alpaca_headers():