207 lines
6.8 KiB
Python
207 lines
6.8 KiB
Python
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
|