- 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)
305 lines
12 KiB
Python
305 lines
12 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
ORB (Opening Range Breakout) Monitor for Futures
|
||
=================================================
|
||
|
||
Monitors session opens across global markets and detects ORB signals.
|
||
|
||
Sessions monitored (ET / UTC-4):
|
||
• Asia (Tokyo) — 19:00 ET (23:00 UTC)
|
||
• London — 03:00 ET (07:00 UTC)
|
||
• New York — 09:30 ET (13:30 UTC)
|
||
|
||
Strategy: After the session opens, the opening range high/low is captured
|
||
over `range_minutes`. If price later breaks above/below that range, an
|
||
ORB signal is logged.
|
||
|
||
⚠️ EDUCATIONAL PURPOSE ONLY — Not financial advice.
|
||
Uses yfinance (delayed data). NOT suitable for live trading.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import sys
|
||
import logging
|
||
from datetime import datetime, timedelta, timezone
|
||
|
||
import yaml
|
||
import pandas as pd
|
||
import yfinance as yf
|
||
|
||
# ─── Constants ────────────────────────────────────────────────────────────────
|
||
|
||
# Yahoo Finance futures tickers
|
||
SYMBOLS = {
|
||
"ES": {"ticker": "ES=F", "name": "S&P 500 E-mini", "multiplier": 0.25},
|
||
"NQ": {"ticker": "NQ=F", "name": "Nasdaq 100 E-mini", "multiplier": 0.25},
|
||
"YM": {"ticker": "YM=F", "name": "Dow E-mini", "multiplier": 0.05},
|
||
"CL": {"ticker": "CL=F", "name": "Crude Oil WTI", "multiplier": 0.01},
|
||
"GC": {"ticker": "GC=F", "name": "Gold", "multiplier": 0.10},
|
||
}
|
||
|
||
# Session open times in UTC (no DST ambiguity)
|
||
SESSIONS = {
|
||
"asia": {"name": "Asia (Tokyo)", "open_utc": 23, "offset_min": 0},
|
||
"london": {"name": "London", "open_utc": 7, "offset_min": 0},
|
||
"ny": {"name": "New York", "open_utc": 13, "offset_min": 30},
|
||
}
|
||
|
||
UTC = timezone.utc
|
||
|
||
logger = logging.getLogger("ORB")
|
||
|
||
|
||
# ─── Helpers ──────────────────────────────────────────────────────────────────
|
||
|
||
def load_config(path: str = "config.yaml") -> dict:
|
||
"""Load YAML configuration."""
|
||
try:
|
||
with open(path) as fh:
|
||
return yaml.safe_load(fh)
|
||
except FileNotFoundError:
|
||
logger.warning("config.yaml not found — using defaults")
|
||
return {}
|
||
|
||
|
||
|
||
|
||
|
||
def session_utc_start(date: datetime, session: dict) -> datetime:
|
||
"""Return UTC datetime when this session opens on the given UTC date."""
|
||
return datetime(date.year, date.month, date.day,
|
||
session["open_utc"], session["offset_min"],
|
||
tzinfo=UTC)
|
||
|
||
|
||
def fetch_data(ticker: str, days: int = 5) -> pd.DataFrame:
|
||
"""Fetch intraday futures data from Yahoo Finance (1-min bars)."""
|
||
end = datetime.now(UTC)
|
||
start = end - timedelta(days=days)
|
||
try:
|
||
df = yf.download(ticker, start=start, end=end,
|
||
interval="1m", progress=False, auto_adjust=True)
|
||
if df.empty:
|
||
logger.warning(f"No data returned for {ticker}")
|
||
return df
|
||
|
||
# Flatten MultiIndex columns (yf sometimes returns ('Close', ticker), etc.)
|
||
if isinstance(df.columns, pd.MultiIndex):
|
||
df.columns = [col[0] for col in df.columns]
|
||
|
||
return df
|
||
except Exception as e:
|
||
logger.error(f"Failed to fetch {ticker}: {e}")
|
||
return pd.DataFrame()
|
||
|
||
|
||
def find_session_bars(df: pd.DataFrame, session_start: datetime,
|
||
range_minutes: int) -> pd.DataFrame | None:
|
||
"""Extract the opening-range bars for a session, if data exists."""
|
||
# Allow ±2 min tolerance for session start
|
||
tolerance = timedelta(minutes=2)
|
||
end_bound = session_start + timedelta(minutes=range_minutes) + tolerance
|
||
mask = (df.index >= session_start - tolerance) & \
|
||
(df.index < end_bound)
|
||
range_bars = df.loc[mask]
|
||
return range_bars if len(range_bars) >= 5 else None # Need meaningful data
|
||
|
||
|
||
def analyze_orb(symbol_key: str, symbol_info: dict, df: pd.DataFrame,
|
||
session_key: str, session_info: dict,
|
||
cfg_orb: dict, date: datetime) -> list[dict]:
|
||
"""Check for ORB signals in the data for a given session date."""
|
||
range_min = cfg_orb.get("range_minutes", 30)
|
||
min_range = cfg_orb.get("min_range_ticks", 4)
|
||
max_range = cfg_orb.get("max_range_ticks", 100)
|
||
multiplier = symbol_info["multiplier"]
|
||
|
||
signals: list[dict] = []
|
||
session_start = session_utc_start(date, session_info)
|
||
|
||
# Try both start date and day before (in case of overnight sessions)
|
||
for offset in [0, -1]:
|
||
check_date = date + timedelta(days=offset)
|
||
try_start = datetime(check_date.year, check_date.month, check_date.day,
|
||
session_info["open_utc"], session_info["offset_min"],
|
||
tzinfo=UTC)
|
||
range_bars = find_session_bars(df, try_start, range_min)
|
||
if range_bars is None:
|
||
continue
|
||
|
||
# Opening range high/low — force scalar extraction
|
||
range_high = range_bars["High"].max().item()
|
||
range_low = range_bars["Low"].min().item()
|
||
range_size = range_high - range_low
|
||
range_ticks = range_size / multiplier
|
||
|
||
if range_ticks < min_range or range_ticks > max_range:
|
||
continue # Skip — range too small or too large
|
||
|
||
# Look for breakout in remaining data after range period
|
||
# Skip NaN rows and only use real data
|
||
remaining = df.loc[range_bars.index[-1]:].dropna(subset=["Close"])
|
||
if remaining.empty:
|
||
continue
|
||
|
||
# Bullish breakout: price closes above range high
|
||
bullish_bars = remaining[remaining["Close"] > range_high]
|
||
if not bullish_bars.empty:
|
||
breakout_time = bullish_bars.index[0]
|
||
breakout_price = float(bullish_bars.loc[breakout_time, "Close"])
|
||
signals.append({
|
||
"symbol": symbol_key,
|
||
"name": symbol_info["name"],
|
||
"session": session_info["name"],
|
||
"direction": "LONG",
|
||
"range_high": round(range_high, 2),
|
||
"range_low": round(range_low, 2),
|
||
"range_size": round(range_size, 2),
|
||
"range_ticks": round(range_ticks, 1),
|
||
"breakout_time": breakout_time,
|
||
"breakout_price": round(breakout_price, 2),
|
||
})
|
||
|
||
# Bearish breakout: price closes below range low
|
||
bearish_bars = remaining[remaining["Close"] < range_low]
|
||
if not bearish_bars.empty:
|
||
breakout_time = bearish_bars.index[0]
|
||
breakout_price = float(bearish_bars.loc[breakout_time, "Close"])
|
||
signals.append({
|
||
"symbol": symbol_key,
|
||
"name": symbol_info["name"],
|
||
"session": session_info["name"],
|
||
"direction": "SHORT",
|
||
"range_high": round(range_high, 2),
|
||
"range_low": round(range_low, 2),
|
||
"range_size": round(range_size, 2),
|
||
"range_ticks": round(range_ticks, 1),
|
||
"breakout_time": breakout_time,
|
||
"breakout_price": round(breakout_price, 2),
|
||
})
|
||
|
||
return signals
|
||
|
||
|
||
# ─── Main ─────────────────────────────────────────────────────────────────────
|
||
|
||
def run(date_str: str | None = None, days: int = 5,
|
||
config_path: str = "config.yaml") -> list[dict]:
|
||
"""
|
||
Analyze ORB patterns for all sessions and symbols.
|
||
|
||
Args:
|
||
date_str: Optional date in YYYY-MM-DD format. If None, uses today.
|
||
days: How many days of history to fetch.
|
||
config_path: Path to config.yaml.
|
||
|
||
Returns:
|
||
List of signal dicts sorted by breakout_time.
|
||
"""
|
||
cfg = load_config(config_path)
|
||
cfg_orb = cfg.get("orb", {})
|
||
|
||
target_date = datetime.strptime(date_str, "%Y-%m-%d").replace(tzinfo=UTC) if date_str else datetime.now(UTC)
|
||
# Expand search window to cover all sessions around the target date
|
||
search_dates = [target_date + timedelta(days=d) for d in range(-1, days)]
|
||
|
||
all_signals: list[dict] = []
|
||
symbol_items = cfg.get("symbols", SYMBOLS) or SYMBOLS
|
||
|
||
for sym_key, sym_info in symbol_items.items():
|
||
ticker = sym_info.get("ticker", f"{sym_key}=F")
|
||
print(f" Fetching {ticker} ({sym_info.get('name', sym_key)}) …", flush=True)
|
||
df = fetch_data(ticker, days=days)
|
||
if df.empty:
|
||
continue
|
||
|
||
# Reconcile multiplier from config vs hardcoded
|
||
sym_info.setdefault("multiplier", SYMBOLS.get(sym_key, {}).get("multiplier", 0.25))
|
||
|
||
for sd in search_dates:
|
||
for sess_key, sess_info in SESSIONS.items():
|
||
signals = analyze_orb(
|
||
sym_key, sym_info, df,
|
||
sess_key, sess_info, cfg_orb, sd
|
||
)
|
||
all_signals.extend(signals)
|
||
|
||
# Sort by breakout time
|
||
all_signals.sort(key=lambda s: s["breakout_time"])
|
||
return all_signals
|
||
|
||
|
||
def format_report(signals: list[dict]) -> str:
|
||
"""Pretty-print ORB signals for Telegram / terminal."""
|
||
if not signals:
|
||
return (
|
||
"📊 **ORB Scan Complete — No Signals Found**\n\n"
|
||
"No opening range breakouts detected in the scanned period.\n"
|
||
"The market may be quiet, or the data may be too delayed.\n\n"
|
||
"*Run again closer to session opens for best results.*"
|
||
)
|
||
|
||
lines = [
|
||
f"📊 **ORB Signals Found** ({len(signals)} signals)",
|
||
f"_Scanned: {datetime.now(UTC).strftime('%Y-%m-%d %H:%M UTC')}_",
|
||
"─" * 40,
|
||
]
|
||
|
||
for s in signals:
|
||
direction = "🟢 LONG" if s["direction"] == "LONG" else "🔴 SHORT"
|
||
lines.append(
|
||
f"\n**{s['symbol']}** ({s['name']}) — {s['session']}\n"
|
||
f"{direction}\n"
|
||
f" Range: {s['range_low']} – {s['range_high']} "
|
||
f"({s['range_size']} pts / {s['range_ticks']} ticks)\n"
|
||
f" Breakout: {s['breakout_price']} at "
|
||
f"`{s['breakout_time'].strftime('%H:%M UTC')}`"
|
||
)
|
||
|
||
lines.append("\n" + "─" * 40)
|
||
lines.append(
|
||
"⚠️ _Educational analysis only. Uses delayed data._\n"
|
||
"_Not financial advice. Verify with live data before trading._"
|
||
)
|
||
return "\n".join(lines)
|
||
|
||
|
||
# ─── CLI ──────────────────────────────────────────────────────────────────────
|
||
|
||
def main():
|
||
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
|
||
|
||
import argparse
|
||
parser = argparse.ArgumentParser(description="ORB Futures Monitor")
|
||
parser.add_argument("--date", type=str, default=None,
|
||
help="Target date YYYY-MM-DD (default: today)")
|
||
parser.add_argument("--days", type=int, default=5,
|
||
help="Days of history to scan (default: 5)")
|
||
parser.add_argument("--config", type=str, default="config.yaml",
|
||
help="Path to config file")
|
||
parser.add_argument("--json", action="store_true",
|
||
help="Output as JSON instead of formatted text")
|
||
args = parser.parse_args()
|
||
|
||
print("\n🔍 ORB Monitor — Scanning futures data …\n", flush=True)
|
||
signals = run(date_str=args.date, days=args.days, config_path=args.config)
|
||
|
||
if args.json:
|
||
import json
|
||
print(json.dumps(signals, indent=2, default=str))
|
||
else:
|
||
report = format_report(signals)
|
||
print(report)
|
||
# Also save to log
|
||
log_path = "orb_signals.log"
|
||
with open(log_path, "a") as fh:
|
||
fh.write(f"\n{'='*50}\n")
|
||
fh.write(f"Scan: {datetime.now(UTC).isoformat()}\n")
|
||
fh.write(report + "\n")
|
||
|
||
print(f"\n✅ Done. {len(signals)} signals detected.\n", flush=True)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|