rmi-backend/app/core/signal_generator.py
opencode c762564d40 style(rmi-backend): complete lint cleanup — 1175→0 ruff errors
- Fix 71 invalid-syntax files (class-body newline-broken assignments)
- Add from/None chain to 307 B904 raise-without-from sites
- Add B008 ignore to ruff.toml (already in pyproject.toml)
- Noqa F401 on __init__.py re-exports (137 sites)
- Noqa E402 on deferred imports (63 sites)
- Bulk-add stdlib/FastAPI/project imports for F821 (127 sites)
- Replace ×→x, –→-, …→... in docstrings (4093 chars)
- Manual refactor of 5 SIM103/SIM116 patterns

Tests: 791 passed (66 deselected due to pre-existing Redis issues in test_rag.py)
Co-authored-by: opencode <opencode@rugmunch.io>
2026-07-06 15:43:20 +02:00

126 lines
4.4 KiB
Python

#!/usr/bin/env python3
"""RMI Signal Generator - Automated trading signals from SENTINEL + market data.
Publishes to Redpanda for real-time consumption. Cron every 5 minutes."""
import asyncio
import json
import os
from datetime import UTC, datetime
import httpx
from app.core.logging import get_logger
logger = get_logger(__name__)
BACKEND = os.getenv("BACKEND_URL", "http://localhost:8000")
REDPANDA = os.getenv("REDPANDA_BROKER", "rmi-redpanda:9092")
TOPIC = "rmi.sentinel.signals"
RMI_KEY = os.getenv("RMI_INTERNAL_KEY", "rmi-internal-2026")
CHAINS = ["solana", "ethereum", "bsc", "base", "arbitrum"]
SIGNAL_RULES = {
"avoid": {"max_safety": 35, "label": "🔴 AVOID", "desc": "High risk - likely scam or honeypot"},
"caution": {"max_safety": 55, "min_safety": 36, "label": "🟡 CAUTION", "desc": "Moderate risk - DYOR carefully"},
"watch": {"min_safety": 56, "max_safety": 75, "label": "🟢 WATCH", "desc": "Decent metrics - worth monitoring"},
"gem": {
"min_safety": 76,
"max_liquidity": 500000,
"label": "💎 GEM",
"desc": "Strong safety, low cap - potential gem",
},
"bluechip": {
"min_safety": 76,
"min_liquidity": 500000,
"label": "🏦 BLUECHIP",
"desc": "Established, high liquidity, safe",
},
}
async def fetch_trending(chain: str, limit: int = 10) -> list:
async with httpx.AsyncClient(timeout=15) as c:
r = await c.get(
f"{BACKEND}/api/v1/databus/fetch/trending?chain={chain}&limit={limit}", headers={"X-RMI-Key": RMI_KEY}
)
if r.status_code == 200:
data = r.json()
return data.get("data", data).get("tokens", []) if isinstance(data, dict) else []
return []
async def scan_and_signal(token: dict) -> dict | None:
addr = token.get("address", "") or token.get("mint", "")
if not addr:
return None
async with httpx.AsyncClient(timeout=20) as c:
r = await c.post(
f"{BACKEND}/api/v1/token/scan",
json={"token_address": addr, "chain": token.get("chain", "solana")},
headers={"X-RMI-Key": RMI_KEY},
)
if r.status_code != 200:
return None
scan = r.json()
score = scan.get("safety_score", 50)
liq = scan.get("free", {}).get("liquidity_usd", 0) or 0
for _rule_name, rule in SIGNAL_RULES.items():
if "max_safety" in rule and score > rule["max_safety"]:
continue
if "min_safety" in rule and score < rule["min_safety"]:
continue
if "max_liquidity" in rule and liq > rule.get("max_liquidity", float("inf")):
continue
if "min_liquidity" in rule and liq < rule.get("min_liquidity", 0):
continue
return {
"timestamp": datetime.now(UTC).isoformat(),
"token": token.get("symbol", "?"),
"address": addr,
"chain": token.get("chain", "?"),
"safety_score": score,
"liquidity_usd": liq,
"signal": rule["label"],
"description": rule["desc"],
"risk_flags": scan.get("risk_flags", []),
}
return None
async def publish_signal(signal: dict):
try:
async with httpx.AsyncClient(timeout=10) as c:
await c.post(
f"http://{REDPANDA}/topics/{TOPIC}",
json={"records": [{"value": json.dumps(signal), "key": signal["address"]}]},
headers={"Content-Type": "application/vnd.kafka.json.v2+json"},
)
except Exception:
pass
async def main():
logger.info(f"Signal Generator - {datetime.now(UTC).isoformat()}")
signals = []
for chain in CHAINS:
tokens = await fetch_trending(chain, 10)
for token in tokens:
signal = await scan_and_signal(token)
if signal:
signals.append(signal)
await publish_signal(signal)
# Sort by interest: gems first, then avoids (people want warnings)
signals.sort(key=lambda s: (0 if "GEM" in s["signal"] else 1 if "AVOID" in s["signal"] else 2, -s["safety_score"]))
for s in signals[:20]:
logger.info(f" {s['signal']} {s['token']} ({s['chain']}) score={s['safety_score']} liq=${s['liquidity_usd']:,.0f}")
logger.info(f"Generated {len(signals)} signals across {len(CHAINS)} chains")
return signals
if __name__ == "__main__":
asyncio.run(main())