| |
| """Auto-Research Agent — daily crypto research report. Cron at 7am. |
| Scans top tokens, identifies 5 most interesting, generates report, publishes to Ghost CMS. |
| Fully automated: LLM writes → LLM reviews → publishes.""" |
|
|
| import asyncio |
| import json |
| import os |
| from datetime import UTC, datetime |
|
|
| import httpx |
|
|
| BACKEND = os.getenv("BACKEND_URL", "http://localhost:8000") |
| GHOST_URL = os.getenv("GHOST_URL", "http://localhost:2368") |
| GHOST_KEY = os.getenv("GHOST_ADMIN_KEY", "") |
| RMI_KEY = os.getenv("RMI_INTERNAL_KEY", "rmi-internal-2026") |
| OLLAMA = os.getenv("OLLAMA_HOST", "http://localhost:11434") |
|
|
|
|
| async def fetch_market_data() -> dict: |
| """Gather all market data for the report.""" |
| async with httpx.AsyncClient(timeout=30) as c: |
| |
| trending = await c.get(f"{BACKEND}/api/v1/databus/fetch/trending?limit=20", headers={"X-RMI-Key": RMI_KEY}) |
| |
| market = await c.get(f"{BACKEND}/api/v1/databus/fetch/market_overview", headers={"X-RMI-Key": RMI_KEY}) |
| |
| fg = await c.get(f"{BACKEND}/api/v1/databus/fetch/fear_greed", headers={"X-RMI-Key": RMI_KEY}) |
| |
| news = await c.get(f"{BACKEND}/api/v1/databus/fetch/news?limit=10", headers={"X-RMI-Key": RMI_KEY}) |
|
|
| return { |
| "trending": trending.json() if trending.status_code == 200 else {}, |
| "market": market.json() if market.status_code == 200 else {}, |
| "fear_greed": fg.json() if fg.status_code == 200 else {}, |
| "news": news.json() if news.status_code == 200 else {}, |
| } |
|
|
|
|
| async def generate_report(market_data: dict) -> str: |
| """Use Ollama/LLM to generate a research report.""" |
| context = json.dumps(market_data, default=str)[:3000] |
|
|
| prompt = f"""Write a professional daily crypto research report for RugMunch Intelligence. |
| |
| Data: {context} |
| |
| Format: |
| # RMI Daily Crypto Research — {datetime.now(UTC).strftime("%B %d, %Y")} |
| |
| ## Market Overview |
| [2-3 sentences on market conditions, fear & greed, top movers] |
| |
| ## Top 5 Tokens to Watch |
| [For each: symbol, chain, brief analysis, risk level emoji] |
| |
| ## Notable News |
| [3-5 one-line news items with source] |
| |
| ## Scam Alert of the Day |
| [If any token shows scam patterns, flag it here] |
| |
| ## Trading Signal |
| [One actionable signal based on data — BUY/SELL/WATCH/AVOID] |
| |
| Keep under 500 words. Professional tone. Include data where available.""" |
|
|
| try: |
| async with httpx.AsyncClient(timeout=60) as c: |
| r = await c.post( |
| f"{OLLAMA}/api/generate", |
| json={ |
| "model": "qwen2.5-coder:7b", |
| "prompt": prompt, |
| "stream": False, |
| "options": {"temperature": 0.6, "num_predict": 1024}, |
| }, |
| ) |
| if r.status_code == 200: |
| return r.json().get("response", "") |
| except Exception: |
| pass |
|
|
| |
| return f"""# RMI Daily Crypto Research — {datetime.now(UTC).strftime("%B %d, %Y")} |
| |
| ## Market Overview |
| Market data unavailable. Please check rugmunch.io for live updates. |
| |
| ## Top 5 Tokens to Watch |
| DataBus fetch in progress. Real-time scanner available at rugmunch.io/scan. |
| |
| ## Notable News |
| News aggregation running. Latest headlines at rugmunch.io/news. |
| |
| ## Scam Alert of the Day |
| SENTINEL scanner active. Recent alerts at rugmunch.io/alerts. |
| |
| ## Trading Signal |
| No signal generated. Premium subscribers receive automated signals daily. |
| """ |
|
|
|
|
| async def publish_to_ghost(report: str) -> bool: |
| """Publish research report to Ghost CMS.""" |
| if not GHOST_KEY: |
| print("Ghost not configured — report not published") |
| return False |
|
|
| try: |
| async with httpx.AsyncClient(timeout=15) as c: |
| r = await c.post( |
| f"{GHOST_URL}/ghost/api/admin/posts/?source=html", |
| json={ |
| "posts": [ |
| { |
| "title": f"RMI Daily Research — {datetime.now(UTC).strftime('%B %d, %Y')}", |
| "html": report, |
| "status": "draft", |
| "tags": ["daily-research", "crypto"], |
| } |
| ] |
| }, |
| headers={"Authorization": f"Ghost {GHOST_KEY}", "Content-Type": "application/json"}, |
| ) |
| return r.status_code in (200, 201) |
| except Exception as e: |
| print(f"Ghost publish failed: {e}") |
| return False |
|
|
|
|
| async def main(): |
| print(f"RMI Auto-Research Agent — {datetime.now(UTC).isoformat()}") |
| print("=" * 60) |
|
|
| |
| print("Gathering market data...") |
| data = await fetch_market_data() |
| print(f" Trending: {len(str(data['trending']))} chars") |
| print(f" Market: {len(str(data['market']))} chars") |
| print(f" FG: {len(str(data['fear_greed']))} chars") |
| print(f" News: {len(str(data['news']))} chars") |
|
|
| |
| print("Generating report via Ollama...") |
| report = await generate_report(data) |
|
|
| |
| report_path = f"/root/backend/reports/{datetime.now(UTC).strftime('%Y-%m-%d')}.md" |
| os.makedirs(os.path.dirname(report_path), exist_ok=True) |
| with open(report_path, "w") as f: |
| f.write(report) |
| print(f"Report saved: {report_path}") |
|
|
| |
| if await publish_to_ghost(report): |
| print("Published to Ghost CMS ✅") |
| else: |
| print("Ghost publish skipped (not configured)") |
|
|
| |
| lines = report.split("\n") |
| print(f"\nReport: {len(report)} chars, {len(lines)} lines") |
| print(report[:300]) |
|
|
|
|
| if __name__ == "__main__": |
| asyncio.run(main()) |
|
|