MK_Quant_Monitor / module_wrappers.py
maxkru92's picture
feat: upload assets/module_wrappers.py
a5b0671 verified
Raw
History Blame Contribute Delete
9.86 kB
"""
Lightweight module wrappers for local asset modules.
Provides safe, minimal wrappers with standardized init() and render(container) API.
Wrappers included:
- HTMLModule: render local .html files (embedded via components.html)
- VolRegimeModule: import vol-regime-prediction-main (if present) and expose run_eda if available; shows pre-generated figures
- CrashMonitorModule: use crash_monitor data fetcher/calculator to produce quick charts (uses synthetic demo data to avoid blocking external calls)
This file avoids heavy imports at top-level; GUI libraries are imported inside render() methods.
"""
from __future__ import annotations
import os
import sys
import traceback
from pathlib import Path
from typing import Optional
class BaseModule:
def __init__(self, name: str, path: str):
self.name = name
self.path = Path(path)
self.available = True
self._error: Optional[str] = None
def init(self) -> None:
"""Initialize the module (no-op by default)."""
return None
def render(self, container) -> None:
"""Render the module into the provided Streamlit container.
The container is expected to be a Streamlit DeltaGenerator used via
"with container:" in the caller.
"""
raise NotImplementedError
class HTMLModule(BaseModule):
def init(self) -> None:
if not self.path.exists():
self.available = False
self._error = f"HTML file not found: {self.path}"
def render(self, container) -> None:
import streamlit as st
import streamlit.components.v1 as components
with container:
if not self.available:
st.error(self._error or "HTML module not available")
return
try:
html = self.path.read_text(encoding="utf-8")
except Exception as e:
st.error(f"Failed to read HTML: {e}")
return
# Inline the HTML in an iframe-like component. Height is conservative.
components.html(html, height=700, scrolling=True)
class VolRegimeModule(BaseModule):
def init(self) -> None:
# We try to import the project's analysis module by adding the project
# root (folder that contains src/) to sys.path.
try:
project_root = self.path
if not project_root.exists():
raise FileNotFoundError(f"Vol-regime project root not found: {project_root}")
# Add project root to sys.path to allow `import src...`
pr = str(project_root)
if pr not in sys.path:
sys.path.insert(0, pr)
# Import lazily
try:
import src.analysis.eda as eda # type: ignore
self._eda = eda
# main visualization entrypoints we might call
self.main_fn = getattr(eda, "run_eda", None)
self.available = True
except Exception as e:
# don't fail hard β€” we'll fallback to showing pre-built figures
self._eda = None
self.main_fn = None
self.available = False
self._error = str(e)
except Exception as e:
self.available = False
self._error = str(e)
def render(self, container) -> None:
import streamlit as st
with container:
st.header("Volatility Regime β€” Vol Regime Prediction")
if self.available and getattr(self, "_eda", None) is not None:
st.success("vol-regime module imported successfully")
# Show quick access to main function if available
if self.main_fn is not None:
if st.button("Run vol-regime main visualization (run_eda)"):
try:
with st.spinner("Running run_eda() β€” may take a while..."):
# run_eda writes figures to reports/figures by default
out = self.main_fn()
st.success("run_eda() completed")
except Exception as e:
st.exception(f"run_eda() failed: {e}")
# Show pre-generated figures if present
figs_dir = self.path / "reports" / "figures"
if figs_dir.exists():
imgs = sorted([p for p in figs_dir.glob("*.png")])
if imgs:
st.markdown("**Pre-generated figures**")
for img in imgs[:6]:
st.image(str(img), use_column_width=True)
else:
st.info("No pre-generated figures found in reports/figures")
else:
st.info("No reports/figures directory found β€” try running the module's run_eda() from its project root")
else:
st.warning("vol-regime module not importable: " + (self._error or "unknown"))
# Also try to show any existing static figures even if import failed
figs_dir = self.path / "reports" / "figures"
if figs_dir.exists():
imgs = sorted([p for p in figs_dir.glob("*.png")])
if imgs:
st.markdown("**Pre-generated figures (import failed, showing static files)**")
for img in imgs[:6]:
st.image(str(img), use_column_width=True)
class CrashMonitorModule(BaseModule):
def init(self) -> None:
# Import crash_monitor helper modules directly from files to avoid
# package-level side-effects (e.g., flask/blueprint imports).
try:
fetcher_path = self.path / 'data' / 'fetcher.py'
calc_path = self.path / 'data' / 'calculator.py'
if not fetcher_path.exists() or not calc_path.exists():
raise FileNotFoundError(f"crash_monitor data modules not found under {self.path}")
import importlib.util
spec_f = importlib.util.spec_from_file_location(f"crash_monitor_fetcher_{self.name}", str(fetcher_path))
fetcher_mod = importlib.util.module_from_spec(spec_f)
spec_f.loader.exec_module(fetcher_mod)
spec_c = importlib.util.spec_from_file_location(f"crash_monitor_calc_{self.name}", str(calc_path))
calc_mod = importlib.util.module_from_spec(spec_c)
spec_c.loader.exec_module(calc_mod)
self.fetcher = fetcher_mod
self.calc = calc_mod
self.available = True
except Exception as e:
self.fetcher = None
self.calc = None
self.available = False
self._error = str(e)
def render(self, container) -> None:
import streamlit as st
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
with container:
st.header("Crash Monitor β€” Quick Charts")
if not (getattr(self, "fetcher", None) and getattr(self, "calc", None)):
st.warning("Crash monitor not available: " + (self._error or "unknown"))
return
try:
# Instantiate DataFetcher but avoid slow external sources β€” use synthetic demo
DataFetcher = getattr(self.fetcher, "DataFetcher")
dfetch = DataFetcher()
try:
# Fast synthetic demo path (guaranteed local)
src = dfetch._synthetic_demo("SPX")
except Exception:
# Fallback to safe fetch (may still be networked)
src = dfetch.fetch_options_chain("SPX")
spot = getattr(src, "spot", None) or src[0] if isinstance(src, (list, tuple)) else None
# src is SourceResult dataclass; it has .strikes and .spot
strikes = getattr(src, "strikes", None) or []
if not strikes:
st.info("No strike data available from crash monitor (empty).")
return
# Crash profile
profile = self.calc.compute_crash_profile(strikes, spot)
dfp = pd.DataFrame(profile).sort_values("spot_pct")
dfp = dfp.set_index("spot_pct")
st.markdown("**GEX+ vs Spot % (Crash Profile)**")
st.line_chart(dfp["gex_plus"])
# Heatmap
grid = self.calc.compute_heatmap_grid(strikes, spot)
values = np.array(grid.get("values", []))
if values.size:
fig, ax = plt.subplots(figsize=(8, 4))
im = ax.imshow(values, aspect="auto", origin="lower", cmap="RdBu_r", vmin=-1, vmax=1)
ax.set_title("GEX+ heatmap (normalized)")
ax.set_xlabel("IV shock bins")
ax.set_ylabel("Spot % bins")
plt.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
st.pyplot(fig)
else:
st.info("Heatmap not available")
except Exception as e:
st.exception(f"Crash monitor render error: {e}\n" + traceback.format_exc())
# Helper factory functions
def _html_module(name: str, path: str) -> HTMLModule:
return HTMLModule(name, path)
def _vol_module(name: str, path: str) -> VolRegimeModule:
return VolRegimeModule(name, path)
def _crash_module(name: str, path: str) -> CrashMonitorModule:
return CrashMonitorModule(name, path)
# Public API β€” simple classes for callers to instantiate
HTMLModule = HTMLModule
VolRegimeModule = VolRegimeModule
CrashMonitorModule = CrashMonitorModule