v1.2 strategy upgrade: small-capital fee-aware signals, two-model pipeline, auto deep-research escalation

This commit is contained in:
2026-02-26 01:38:55 -08:00
parent 080e66980f
commit cb5edf694d
4 changed files with 153 additions and 48 deletions

View File

@@ -4,7 +4,20 @@ import requests
from config import settings
def searx_news(symbol: str, limit: int = 10):
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:
@@ -13,75 +26,129 @@ def searx_news(symbol: str, limit: int = 10):
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", "")[:500]})
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):
payload = {
"model": settings.ollama_model,
"stream": False,
"prompt": json.dumps({
"task": "Summarize market-moving info into a concise, neutral brief.",
"symbol": symbol,
"news": context_items,
"format": {"summary": "<=140 words", "bullish_points": ["..."], "bearish_points": ["..."]}
}),
"format": "json",
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:
r = requests.post(f"{settings.ollama_url}/api/generate", json=payload, timeout=45)
r.raise_for_status()
resp = r.json().get("response", "{}")
parsed = json.loads(resp)
parsed = _ollama_generate(settings.ollama_curator_model, prompt)
return parsed.get("summary", "no-summary")
except Exception:
return "summary-unavailable"
def ollama_decide(symbol: str, context_items: list):
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": "You are a conservative autonomous trading policy engine. Return strict JSON only.",
"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,
"risk": "do not overtrade; prefer hold on weak signal",
"min_expected_edge_after_fees": "positive",
"fee_per_trade_usd": settings.fee_per_trade_usd,
"slippage_bps": settings.slippage_bps,
"avoid_overtrading": True,
},
"symbol": symbol,
"strategy_prior": strategy,
"news": context_items,
"output_schema": {
"action": "buy|sell|hold",
"confidence": "0-1",
"reason": "short rationale",
"order_usd": f"<= {settings.max_order_usd}",
"reason": "short rationale",
"needs_more_research": True,
"research_topics": ["..."]
},
}
payload = {
"model": settings.ollama_model,
"prompt": json.dumps(prompt),
"stream": False,
"format": "json",
}
try:
r = requests.post(f"{settings.ollama_url}/api/generate", json=payload, timeout=45)
r.raise_for_status()
resp = r.json().get("response", "{}")
d = json.loads(resp)
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}
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": random.choice(["hold", "hold", "buy"]),
"confidence": 0.3,
"action": strategy.get("action", "hold"),
"confidence": min(strategy.get("confidence", 0.5), 0.55),
"order_usd": min(1.0, settings.max_order_usd),
"reason": "fallback-mode",
"reason": "decision-fallback-strategy",
"needs_more_research": False,
"research_topics": [],
}