import json import random import requests from config import settings def _ollama_generate(model: str, payload_obj: dict, timeout: int = 45): payload = { "model": model, "stream": False, "prompt": json.dumps(payload_obj), "format": "json", } r = requests.post(f"{settings.ollama_url}/api/generate", json=payload, timeout=timeout) r.raise_for_status() resp = r.json().get("response", "{}") return json.loads(resp) def searx_news(symbol: str, limit: int = 12): q = f"{symbol} stock news earnings guidance analyst macro risk" params = {"q": q, "format": "json", "language": "en"} try: r = requests.get(settings.searx_url, params=params, timeout=20) r.raise_for_status() data = r.json() out = [] for it in data.get("results", [])[:limit]: out.append({ "title": it.get("title", ""), "url": it.get("url", ""), "content": (it.get("content", "") or "")[:700], }) return out except Exception: return [] def extra_research(symbol: str, weak_points: list, limit: int = 6): """Second-pass targeted research when confidence/coverage is weak.""" q = f"{symbol} {' '.join(weak_points[:3])} SEC filing guidance risks competition" params = {"q": q, "format": "json", "language": "en"} try: r = requests.get(settings.searx_url, params=params, timeout=20) r.raise_for_status() data = r.json() out = [] for it in data.get("results", [])[:limit]: out.append({ "title": it.get("title", ""), "url": it.get("url", ""), "content": (it.get("content", "") or "")[:700], }) return out except Exception: return [] def summarize_news_with_ollama(symbol: str, context_items: list): prompt = { "task": "Summarize market-moving info into a concise, neutral brief.", "symbol": symbol, "news": context_items, "format": { "summary": "<=140 words", "bullish_points": ["..."], "bearish_points": ["..."], "uncertainties": ["..."] } } try: parsed = _ollama_generate(settings.ollama_curator_model, prompt) return parsed.get("summary", "no-summary") except Exception: return "summary-unavailable" def strategy_signals(symbol: str, context_items: list): """Proven-ish small-capital rules encoded as interpretable signals.""" text_blob = " ".join((x.get("title", "") + " " + x.get("content", "")) for x in context_items).lower() bullish = sum(k in text_blob for k in ["beat", "raise guidance", "upgrade", "buyback", "record revenue"]) bearish = sum(k in text_blob for k in ["miss", "downgrade", "lawsuit", "probe", "cut guidance", "recall"]) # simple event momentum score score = bullish - bearish # conservative policy for tiny capital: only trade stronger score edges if score >= 2: action = "buy" elif score <= -2: action = "sell" else: action = "hold" conf = min(0.85, 0.50 + abs(score) * 0.08) return { "strategy": "event-momentum-v1", "score": score, "action": action, "confidence": conf, "signals": {"bullish": bullish, "bearish": bearish}, } def llm_final_decision(symbol: str, context_items: list, strategy: dict): prompt = { "task": "Final trading decision using all context and a conservative small-capital profile. Return strict JSON.", "symbol": symbol, "constraints": { "actions": ["buy", "sell", "hold"], "max_order_usd": settings.max_order_usd, "min_expected_edge_after_fees": "positive", "fee_per_trade_usd": settings.fee_per_trade_usd, "slippage_bps": settings.slippage_bps, "avoid_overtrading": True, }, "strategy_prior": strategy, "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": ["..."] }, } 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" 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") 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 [], } 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": [], } def alpaca_headers(): return { "APCA-API-KEY-ID": settings.alpaca_key, "APCA-API-SECRET-KEY": settings.alpaca_secret, "Content-Type": "application/json", } def place_order(symbol: str, action: str, order_usd: float): if action not in {"buy", "sell"}: return None payload = { "symbol": symbol, "side": action, "type": "market", "time_in_force": "day", "notional": round(order_usd, 2), } try: r = requests.post(f"{settings.alpaca_base}/v2/orders", headers=alpaca_headers(), json=payload, timeout=20) return {"ok": r.ok, "status": r.status_code, "json": r.json() if r.text else {}} except Exception as e: return {"ok": False, "status": 0, "json": {"error": str(e)}} def account_snapshot(): try: r = requests.get(f"{settings.alpaca_base}/v2/account", headers=alpaca_headers(), timeout=20) r.raise_for_status() return r.json() except Exception: return {} def positions_snapshot(): try: r = requests.get(f"{settings.alpaca_base}/v2/positions", headers=alpaca_headers(), timeout=20) if r.ok: return r.json() except Exception: pass return [] def market_open(): try: r = requests.get(f"{settings.alpaca_base}/v2/clock", headers=alpaca_headers(), timeout=20) if r.ok: return bool(r.json().get("is_open", False)) except Exception: pass return False