- Install Prometheus + Grafana via kube-prometheus-stack (ClusterIP only, no public ingress) - Deploy Tailscale Operator for secure VPN access to internal services - Add CNPG/PostgreSQL monitoring dashboards - Add vLLM inference monitoring dashboards (tokens, latency, GPU) - Add Cilium networking dashboards (policy, traffic, drops) - Update infra-controllers staging kustomization to include all controllers - Add monitoring-configs Flux sync for dashboard deployment - Update README with monitoring architecture and access instructions - Remove broken stale monitoring files (Azure Key Vault refs, wrong domains) Access: kubectl port-forward or Tailscale VPN (replace auth key before deploy)
205 lines
7.3 KiB
Python
205 lines
7.3 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
VWAP Wave Breach Scanner
|
||
========================
|
||
Continuous monitoring daemon. Scans instruments on a schedule,
|
||
detects VWAP wave breaches, and pushes alerts via Telegram.
|
||
|
||
Environment variables:
|
||
TELEGRAM_BOT_TOKEN — Telegram Bot API token (required)
|
||
TELEGRAM_CHAT_ID — Chat ID to send alerts to (required)
|
||
SCAN_INTERVAL_SEC — Seconds between scans (default: 120)
|
||
BREACH_THRESHOLD — Sigma threshold for alerts (default: 2.0)
|
||
|
||
No LLM overhead — pure Python, ~20MB RAM.
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
import time
|
||
import logging
|
||
from datetime import datetime, timezone
|
||
|
||
import requests
|
||
import yfinance as yf
|
||
import pandas as pd
|
||
from apscheduler.schedulers.background import BlockingScheduler
|
||
|
||
# ── Config ──────────────────────────────────────────────────────────────────
|
||
|
||
TELEGRAM_BOT_TOKEN = os.environ["TELEGRAM_BOT_TOKEN"]
|
||
TELEGRAM_CHAT_ID = os.environ["TELEGRAM_CHAT_ID"]
|
||
SCAN_INTERVAL_SEC = int(os.environ.get("SCAN_INTERVAL_SEC", "120"))
|
||
BREACH_THRESHOLD = float(os.environ.get("BREACH_THRESHOLD", "2.0"))
|
||
|
||
INSTRUMENTS = {
|
||
"Gold Futures": {"ticker": "GC=F", "decimal": 2},
|
||
"NASDAQ": {"ticker": "^IXIC", "decimal": 2},
|
||
"S&P 500": {"ticker": "^GSPC", "decimal": 2},
|
||
"Crude Oil": {"ticker": "CL=F", "decimal": 2},
|
||
}
|
||
|
||
logging.basicConfig(
|
||
level=logging.INFO,
|
||
format="%(asctime)s %(levelname)-5s %(message)s",
|
||
datefmt="%Y-%m-%d %H:%M:%S",
|
||
)
|
||
log = logging.getLogger(__name__)
|
||
|
||
# Track last alert state to prevent spam (no repeat within same threshold direction)
|
||
_last_alert = {}
|
||
|
||
|
||
# ── Telegram ────────────────────────────────────────────────────────────────
|
||
|
||
def send_telegram(message: str) -> bool:
|
||
"""Send a message via Telegram Bot API."""
|
||
url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
|
||
payload = {
|
||
"chat_id": TELEGRAM_CHAT_ID,
|
||
"text": message,
|
||
"parse_mode": "Markdown",
|
||
"disable_web_page_preview": True,
|
||
}
|
||
try:
|
||
resp = requests.post(url, json=payload, timeout=10)
|
||
resp.raise_for_status()
|
||
log.info("Telegram alert sent: %s", message[:80])
|
||
return True
|
||
except Exception as e:
|
||
log.error("Telegram send failed: %s", e)
|
||
return False
|
||
|
||
|
||
# ── VWAP Calculation ────────────────────────────────────────────────────────
|
||
|
||
def compute_vwap(data: pd.DataFrame) -> dict | None:
|
||
"""Compute cumulative VWAP, σ, and deviation."""
|
||
df = data.copy()
|
||
df["typical"] = (df["High"] + df["Low"] + df["Close"]) / 3.0
|
||
df["tp_vol"] = df["typical"] * df["Volume"]
|
||
|
||
cum_tp_vol = df["tp_vol"].cumsum()
|
||
cum_vol = df["Volume"].cumsum().replace(0, 1)
|
||
|
||
df["cum_vwap"] = cum_tp_vol / cum_vol
|
||
df["deviation"] = df["typical"] - df["cum_vwap"]
|
||
df["cum_var"] = (df["deviation"] ** 2).cumsum() / cum_vol
|
||
df["sigma"] = df["cum_var"] ** 0.5
|
||
|
||
last = df.iloc[-1]
|
||
if last["sigma"] <= 0:
|
||
return None
|
||
|
||
return {
|
||
"price": last["Close"],
|
||
"vwap": last["cum_vwap"],
|
||
"sigma": last["sigma"],
|
||
"dev_sigmas": (last["Close"] - last["cum_vwap"]) / last["sigma"],
|
||
}
|
||
|
||
|
||
def fetch_data(ticker: str) -> pd.DataFrame:
|
||
"""Fetch recent intraday data via yfinance."""
|
||
try:
|
||
data = yf.Ticker(ticker).history(period="1d", interval="1m", auto_adjust=True)
|
||
except Exception:
|
||
data = pd.DataFrame()
|
||
|
||
if len(data) < 30:
|
||
data = yf.Ticker(ticker).history(period="5d", interval="1m", auto_adjust=True)
|
||
cutoff = pd.Timestamp.now(tz=data.index.tz) - pd.Timedelta(hours=24)
|
||
data = data[data.index >= cutoff]
|
||
|
||
return data
|
||
|
||
|
||
# ── Scanner ─────────────────────────────────────────────────────────────────
|
||
|
||
def run_scan() -> None:
|
||
"""Execute a full scan cycle and push any breaches."""
|
||
ts = datetime.now(timezone.utc).strftime("%H:%M:%S UTC")
|
||
log.info("── Scan %s ──", ts)
|
||
|
||
breaches = []
|
||
|
||
for name, cfg in INSTRUMENTS.items():
|
||
try:
|
||
data = fetch_data(cfg["ticker"])
|
||
if data.empty or len(data) < 20:
|
||
log.warning("SKIP %s — insufficient data (%d bars)", name, len(data))
|
||
continue
|
||
|
||
info = compute_vwap(data)
|
||
if info is None:
|
||
log.warning("SKIP %s — zero sigma", name)
|
||
continue
|
||
|
||
d = cfg["decimal"]
|
||
dev = info["dev_sigmas"]
|
||
log.info(" %-14s $%10.2f | VWAP $%10.2f | %+.2fσ", name, info["price"], info["vwap"], dev)
|
||
|
||
if abs(dev) >= BREACH_THRESHOLD:
|
||
# Prevent repeat spam: only alert if state changed
|
||
alert_key = f"{name}:{dev > 0}"
|
||
if _last_alert.get(alert_key) == "breach":
|
||
log.info(" → %s already in breach, skipping repeat", name)
|
||
continue
|
||
|
||
breaches.append((name, cfg["decimal"], dev, info["price"], info["vwap"], info["sigma"]))
|
||
_last_alert[alert_key] = "breach"
|
||
else:
|
||
_last_alert[f"{name}:True"] = "clean"
|
||
_last_alert[f"{name}:False"] = "clean"
|
||
|
||
except Exception as e:
|
||
log.error("ERROR %s: %s", name, e)
|
||
|
||
# Push alerts
|
||
for name, d, dev, price, vwap, sigma in breaches:
|
||
direction = "⬆️ UP" if dev > 0 else "⬇️ DOWN"
|
||
band_label = f"±{int(abs(dev))}σ"
|
||
severity = "🚨 **EXTREME**" if abs(dev) >= 3.0 else "⚡ **BREACH**"
|
||
|
||
msg = (
|
||
f"{severity} — VWAP Wave Alert\n\n"
|
||
f"**{name}** broke through **{band_label}** band\n"
|
||
f"Deviation: **{dev:+.2f}σ**\n"
|
||
f"Price: **${price:.{d}f}** | VWAP: **${vwap:.{d}f}**\n"
|
||
f"σ: ${sigma:.{d}f} | {direction}\n\n"
|
||
f"_at {ts}_"
|
||
)
|
||
send_telegram(msg)
|
||
|
||
|
||
# ── Main ────────────────────────────────────────────────────────────────────
|
||
|
||
def main() -> None:
|
||
log.info("=" * 60)
|
||
log.info(" VWAP Wave Breach Scanner")
|
||
log.info(" Interval: %d sec | Threshold: ±%.1fσ", SCAN_INTERVAL_SEC, BREACH_THRESHOLD)
|
||
log.info(" Telegram: @chat_id=%s", TELEGRAM_CHAT_ID)
|
||
log.info("=" * 60)
|
||
|
||
# Validate Telegram connectivity
|
||
send_telegram(
|
||
"🟢 *VWAP Breach Scanner* is online.\n"
|
||
f"Scanning every **{SCAN_INTERVAL_SEC}s** — threshold ±**{BREACH_THRESHOLD:.1f}σ**\n"
|
||
f"Monitoring: Gold, NASDAQ, S&P 500, Crude Oil"
|
||
)
|
||
|
||
scheduler = BlockingScheduler()
|
||
scheduler.add_job(run_scan, "interval", seconds=SCAN_INTERVAL_SEC, id="scan")
|
||
# Run immediately on start
|
||
run_scan()
|
||
|
||
log.info("Scanner running. Press Ctrl+C to stop.")
|
||
try:
|
||
scheduler.start()
|
||
except KeyboardInterrupt:
|
||
log.info("Shutting down...")
|
||
scheduler.shutdown()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|