#!/usr/bin/env python3 """SOL price alert bot using CoinGecko. Configurable thresholds via JSON.""" from __future__ import annotations import json import logging import sys import time import urllib.error import urllib.request from pathlib import Path COINGECKO_URL = "https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd" def setup_logging(log_file: str) -> logging.Logger: logger = logging.getLogger("price_alert") logger.setLevel(logging.INFO) logger.handlers.clear() fmt = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s") fh = logging.FileHandler(log_file, encoding="utf-8") fh.setFormatter(fmt) sh = logging.StreamHandler(sys.stdout) sh.setFormatter(fmt) logger.addHandler(fh) logger.addHandler(sh) return logger def load_config(path: str) -> dict: with open(path, encoding="utf-8") as f: cfg = json.load(f) required = ["high_threshold", "low_threshold", "poll_interval_seconds"] for key in required: if key not in cfg: raise ValueError(f"Missing config key: {key}") if cfg["high_threshold"] <= cfg["low_threshold"]: raise ValueError("high_threshold must be greater than low_threshold") return cfg def fetch_sol_price(timeout: int = 20) -> float: req = urllib.request.Request( COINGECKO_URL, headers={"Accept": "application/json", "User-Agent": "awa-price-alert/1.0"}, ) with urllib.request.urlopen(req, timeout=timeout) as resp: data = json.loads(resp.read().decode()) price = data.get("solana", {}).get("usd") if price is None: raise RuntimeError(f"Unexpected CoinGecko response: {data}") return float(price) def send_alert(logger: logging.Logger, message: str, webhook_url: str | None) -> None: logger.warning("ALERT: %s", message) if not webhook_url: return body = json.dumps({"content": message}).encode() req = urllib.request.Request( webhook_url, data=body, headers={"Content-Type": "application/json", "User-Agent": "awa-price-alert/1.0"}, method="POST", ) try: with urllib.request.urlopen(req, timeout=15) as resp: logger.info("Webhook status %s", resp.status) except urllib.error.URLError as exc: logger.error("Webhook failed: %s", exc) def run(config_path: str = "config.json") -> None: cfg = load_config(config_path) logger = setup_logging(cfg.get("log_file", "price_alert.log")) high = float(cfg["high_threshold"]) low = float(cfg["low_threshold"]) interval = int(cfg["poll_interval_seconds"]) webhook = cfg.get("webhook_url") state = {"above_high": False, "below_low": False} logger.info( "Monitoring SOL | high=%.4f low=%.4f interval=%ss", high, low, interval, ) while True: try: price = fetch_sol_price() logger.info("SOL price: $%.4f", price) if price >= high and not state["above_high"]: send_alert(logger, f"SOL crossed HIGH ${high:.4f} (now ${price:.4f})", webhook) state["above_high"] = True state["below_low"] = False elif price <= low and not state["below_low"]: send_alert(logger, f"SOL crossed LOW ${low:.4f} (now ${price:.4f})", webhook) state["below_low"] = True state["above_high"] = False elif low < price < high: state["above_high"] = False state["below_low"] = False except Exception as exc: logger.error("Price check failed: %s", exc) time.sleep(interval) if __name__ == "__main__": cfg_path = sys.argv[1] if len(sys.argv) > 1 else str(Path(__file__).with_name("config.json")) run(cfg_path)