Update app.py
Browse files
app.py
CHANGED
|
@@ -18,12 +18,10 @@ import gradio as gr
|
|
| 18 |
APP_TITLE = "ALCO Liquidity & Interest-Rate Risk Dashboard"
|
| 19 |
TABLE_FQN = "my_db.main.masterdataset_v" # source table
|
| 20 |
VIEW_FQN = "my_db.main.positions_v" # normalized view created by this app
|
| 21 |
-
EXPORT_DIR = Path("exports")
|
| 22 |
-
EXPORT_DIR.mkdir(exist_ok=True)
|
| 23 |
|
| 24 |
PRODUCT_ASSETS = [
|
| 25 |
"loan", "overdraft", "advances", "bills", "bill",
|
| 26 |
-
"tbond", "t-bond", "tbill", "t-bill", "repo_asset"
|
| 27 |
]
|
| 28 |
PRODUCT_SOF = [
|
| 29 |
"fd", "term_deposit", "td", "savings", "current",
|
|
@@ -40,11 +38,22 @@ def connect_md() -> duckdb.DuckDBPyConnection:
|
|
| 40 |
return duckdb.connect(f"md:?motherduck_token={token}")
|
| 41 |
|
| 42 |
def discover_columns(conn: duckdb.DuckDBPyConnection, table_fqn: str) -> List[str]:
|
| 43 |
-
#
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
def build_view_sql(existing_cols: List[str]) -> str:
|
| 50 |
wanted = [
|
|
@@ -64,8 +73,10 @@ def build_view_sql(existing_cols: List[str]) -> str:
|
|
| 64 |
|
| 65 |
sof_list = ", ".join([f"'{p}'" for p in PRODUCT_SOF])
|
| 66 |
asset_list = ", ".join([f"'{p}'" for p in PRODUCT_ASSETS])
|
|
|
|
| 67 |
bucket_case = (
|
| 68 |
-
f"CASE
|
|
|
|
| 69 |
f"WHEN lower(product) IN ({asset_list}) THEN 'Assets' "
|
| 70 |
f"ELSE 'Unknown' END AS bucket"
|
| 71 |
)
|
|
@@ -167,56 +178,41 @@ def irr_sql(cols: List[str]) -> str:
|
|
| 167 |
GROUP BY bucket;
|
| 168 |
"""
|
| 169 |
|
| 170 |
-
def export_excel(as_of: str, assets_t1: float, sof_t1: float, net_gap_t1: float,
|
| 171 |
-
ladder: pd.DataFrame, irr: pd.DataFrame) -> Path:
|
| 172 |
-
out = EXPORT_DIR / f"alco_report_{as_of or 'NA'}.xlsx"
|
| 173 |
-
with pd.ExcelWriter(out, engine="xlsxwriter") as xw:
|
| 174 |
-
pd.DataFrame({
|
| 175 |
-
"as_of_date": [as_of or "N/A"],
|
| 176 |
-
"assets_t1": [assets_t1],
|
| 177 |
-
"sof_t1": [sof_t1],
|
| 178 |
-
"net_gap_t1": [net_gap_t1],
|
| 179 |
-
}).to_excel(xw, index=False, sheet_name="kpis")
|
| 180 |
-
(ladder if ladder is not None else pd.DataFrame()).to_excel(xw, index=False, sheet_name="ladder")
|
| 181 |
-
(irr if irr is not None else pd.DataFrame()).to_excel(xw, index=False, sheet_name="irr")
|
| 182 |
-
return out
|
| 183 |
-
|
| 184 |
# =========================
|
| 185 |
# Dashboard callback
|
| 186 |
# =========================
|
| 187 |
-
def run_dashboard() -> Tuple[str, str, str, str, str, Any, pd.DataFrame, pd.DataFrame
|
| 188 |
"""
|
| 189 |
Returns:
|
| 190 |
-
status, as_of, assets_t1, sof_t1, net_gap_t1, figure, ladder_df, irr_df
|
| 191 |
-
(
|
| 192 |
"""
|
| 193 |
try:
|
| 194 |
conn = connect_md()
|
| 195 |
|
| 196 |
-
# Discover columns & build view
|
| 197 |
cols = discover_columns(conn, TABLE_FQN)
|
| 198 |
ensure_view(conn, cols)
|
| 199 |
|
| 200 |
-
#
|
| 201 |
as_of = "N/A"
|
| 202 |
if "as_of_date" in cols:
|
| 203 |
tmp = conn.execute(f"SELECT max(as_of_date) AS d FROM {VIEW_FQN}").fetchdf()
|
| 204 |
if not tmp.empty and not pd.isna(tmp["d"].iloc[0]):
|
| 205 |
as_of = str(tmp["d"].iloc[0])[:10]
|
| 206 |
|
| 207 |
-
# KPIs
|
| 208 |
kpi = conn.execute(KPI_SQL).fetchdf()
|
| 209 |
assets_t1 = safe_num(kpi["assets_t1"].iloc[0]) if not kpi.empty else 0.0
|
| 210 |
sof_t1 = safe_num(kpi["sof_t1"].iloc[0]) if not kpi.empty else 0.0
|
| 211 |
net_gap = safe_num(kpi["net_gap_t1"].iloc[0]) if not kpi.empty else 0.0
|
| 212 |
|
| 213 |
-
# Ladder & IRR
|
| 214 |
ladder = conn.execute(LADDER_SQL).fetchdf()
|
| 215 |
irr = conn.execute(irr_sql(cols)).fetchdf()
|
| 216 |
|
|
|
|
| 217 |
fig = plot_ladder(ladder)
|
| 218 |
-
xlsx = export_excel(as_of, assets_t1, sof_t1, net_gap, ladder, irr)
|
| 219 |
-
xlsx_str = str(xlsx if xlsx.exists() else "")
|
| 220 |
|
| 221 |
status = "β
OK"
|
| 222 |
return (
|
|
@@ -228,12 +224,10 @@ def run_dashboard() -> Tuple[str, str, str, str, str, Any, pd.DataFrame, pd.Data
|
|
| 228 |
fig,
|
| 229 |
ladder,
|
| 230 |
irr,
|
| 231 |
-
xlsx_str,
|
| 232 |
)
|
| 233 |
|
| 234 |
except Exception as e:
|
| 235 |
tb = traceback.format_exc()
|
| 236 |
-
# Return placeholders + human-readable error in status
|
| 237 |
empty_df = pd.DataFrame()
|
| 238 |
fig = plot_ladder(empty_df)
|
| 239 |
return (
|
|
@@ -245,7 +239,6 @@ def run_dashboard() -> Tuple[str, str, str, str, str, Any, pd.DataFrame, pd.Data
|
|
| 245 |
fig,
|
| 246 |
empty_df,
|
| 247 |
empty_df,
|
| 248 |
-
"",
|
| 249 |
)
|
| 250 |
|
| 251 |
# =========================
|
|
@@ -254,7 +247,7 @@ def run_dashboard() -> Tuple[str, str, str, str, str, Any, pd.DataFrame, pd.Data
|
|
| 254 |
with gr.Blocks(title=APP_TITLE) as demo:
|
| 255 |
gr.Markdown(f"# {APP_TITLE}\n_Source:_ `{TABLE_FQN}` β `{VIEW_FQN}`")
|
| 256 |
|
| 257 |
-
status = gr.Textbox(label="Status", interactive=False, lines=
|
| 258 |
|
| 259 |
with gr.Row():
|
| 260 |
refresh_btn = gr.Button("π Refresh", variant="primary")
|
|
@@ -270,11 +263,10 @@ with gr.Blocks(title=APP_TITLE) as demo:
|
|
| 270 |
chart = gr.Plot(label="Maturity Ladder")
|
| 271 |
ladder_df = gr.Dataframe(label="Ladder Detail")
|
| 272 |
irr_df = gr.Dataframe(label="Interest-Rate Risk (approx)")
|
| 273 |
-
excel_file = gr.File(label="Excel export", interactive=False)
|
| 274 |
|
| 275 |
refresh_btn.click(
|
| 276 |
fn=run_dashboard,
|
| 277 |
-
outputs=[status, as_of, a1, a2, a3, chart, ladder_df, irr_df
|
| 278 |
)
|
| 279 |
|
| 280 |
if __name__ == "__main__":
|
|
|
|
| 18 |
APP_TITLE = "ALCO Liquidity & Interest-Rate Risk Dashboard"
|
| 19 |
TABLE_FQN = "my_db.main.masterdataset_v" # source table
|
| 20 |
VIEW_FQN = "my_db.main.positions_v" # normalized view created by this app
|
|
|
|
|
|
|
| 21 |
|
| 22 |
PRODUCT_ASSETS = [
|
| 23 |
"loan", "overdraft", "advances", "bills", "bill",
|
| 24 |
+
"tbond", "t-bond", "tbill", "t-bill", "repo_asset"
|
| 25 |
]
|
| 26 |
PRODUCT_SOF = [
|
| 27 |
"fd", "term_deposit", "td", "savings", "current",
|
|
|
|
| 38 |
return duckdb.connect(f"md:?motherduck_token={token}")
|
| 39 |
|
| 40 |
def discover_columns(conn: duckdb.DuckDBPyConnection, table_fqn: str) -> List[str]:
|
| 41 |
+
# Try DESCRIBE first (fast), fall back to information_schema
|
| 42 |
+
try:
|
| 43 |
+
df = conn.execute(f"DESCRIBE {table_fqn};").fetchdf()
|
| 44 |
+
name_col = "column_name" if "column_name" in df.columns else df.columns[0]
|
| 45 |
+
return [str(c).lower() for c in df[name_col].tolist()]
|
| 46 |
+
except Exception:
|
| 47 |
+
df = conn.execute(
|
| 48 |
+
f"""
|
| 49 |
+
SELECT lower(column_name) AS col
|
| 50 |
+
FROM information_schema.columns
|
| 51 |
+
WHERE table_catalog = split_part('{table_fqn}', '.', 1)
|
| 52 |
+
AND table_schema = split_part('{table_fqn}', '.', 2)
|
| 53 |
+
AND table_name = split_part('{table_fqn}', '.', 3)
|
| 54 |
+
"""
|
| 55 |
+
).fetchdf()
|
| 56 |
+
return df["col"].tolist()
|
| 57 |
|
| 58 |
def build_view_sql(existing_cols: List[str]) -> str:
|
| 59 |
wanted = [
|
|
|
|
| 73 |
|
| 74 |
sof_list = ", ".join([f"'{p}'" for p in PRODUCT_SOF])
|
| 75 |
asset_list = ", ".join([f"'{p}'" for p in PRODUCT_ASSETS])
|
| 76 |
+
|
| 77 |
bucket_case = (
|
| 78 |
+
f"CASE "
|
| 79 |
+
f"WHEN lower(product) IN ({sof_list}) THEN 'SoF' "
|
| 80 |
f"WHEN lower(product) IN ({asset_list}) THEN 'Assets' "
|
| 81 |
f"ELSE 'Unknown' END AS bucket"
|
| 82 |
)
|
|
|
|
| 178 |
GROUP BY bucket;
|
| 179 |
"""
|
| 180 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
# =========================
|
| 182 |
# Dashboard callback
|
| 183 |
# =========================
|
| 184 |
+
def run_dashboard() -> Tuple[str, str, str, str, str, Any, pd.DataFrame, pd.DataFrame]:
|
| 185 |
"""
|
| 186 |
Returns:
|
| 187 |
+
status, as_of, assets_t1, sof_t1, net_gap_t1, figure, ladder_df, irr_df
|
| 188 |
+
(text KPIs to avoid component type errors)
|
| 189 |
"""
|
| 190 |
try:
|
| 191 |
conn = connect_md()
|
| 192 |
|
| 193 |
+
# 1) Discover columns & build view
|
| 194 |
cols = discover_columns(conn, TABLE_FQN)
|
| 195 |
ensure_view(conn, cols)
|
| 196 |
|
| 197 |
+
# 2) As-of (optional)
|
| 198 |
as_of = "N/A"
|
| 199 |
if "as_of_date" in cols:
|
| 200 |
tmp = conn.execute(f"SELECT max(as_of_date) AS d FROM {VIEW_FQN}").fetchdf()
|
| 201 |
if not tmp.empty and not pd.isna(tmp["d"].iloc[0]):
|
| 202 |
as_of = str(tmp["d"].iloc[0])[:10]
|
| 203 |
|
| 204 |
+
# 3) KPIs
|
| 205 |
kpi = conn.execute(KPI_SQL).fetchdf()
|
| 206 |
assets_t1 = safe_num(kpi["assets_t1"].iloc[0]) if not kpi.empty else 0.0
|
| 207 |
sof_t1 = safe_num(kpi["sof_t1"].iloc[0]) if not kpi.empty else 0.0
|
| 208 |
net_gap = safe_num(kpi["net_gap_t1"].iloc[0]) if not kpi.empty else 0.0
|
| 209 |
|
| 210 |
+
# 4) Ladder & IRR
|
| 211 |
ladder = conn.execute(LADDER_SQL).fetchdf()
|
| 212 |
irr = conn.execute(irr_sql(cols)).fetchdf()
|
| 213 |
|
| 214 |
+
# 5) Chart
|
| 215 |
fig = plot_ladder(ladder)
|
|
|
|
|
|
|
| 216 |
|
| 217 |
status = "β
OK"
|
| 218 |
return (
|
|
|
|
| 224 |
fig,
|
| 225 |
ladder,
|
| 226 |
irr,
|
|
|
|
| 227 |
)
|
| 228 |
|
| 229 |
except Exception as e:
|
| 230 |
tb = traceback.format_exc()
|
|
|
|
| 231 |
empty_df = pd.DataFrame()
|
| 232 |
fig = plot_ladder(empty_df)
|
| 233 |
return (
|
|
|
|
| 239 |
fig,
|
| 240 |
empty_df,
|
| 241 |
empty_df,
|
|
|
|
| 242 |
)
|
| 243 |
|
| 244 |
# =========================
|
|
|
|
| 247 |
with gr.Blocks(title=APP_TITLE) as demo:
|
| 248 |
gr.Markdown(f"# {APP_TITLE}\n_Source:_ `{TABLE_FQN}` β `{VIEW_FQN}`")
|
| 249 |
|
| 250 |
+
status = gr.Textbox(label="Status", interactive=False, lines=8)
|
| 251 |
|
| 252 |
with gr.Row():
|
| 253 |
refresh_btn = gr.Button("π Refresh", variant="primary")
|
|
|
|
| 263 |
chart = gr.Plot(label="Maturity Ladder")
|
| 264 |
ladder_df = gr.Dataframe(label="Ladder Detail")
|
| 265 |
irr_df = gr.Dataframe(label="Interest-Rate Risk (approx)")
|
|
|
|
| 266 |
|
| 267 |
refresh_btn.click(
|
| 268 |
fn=run_dashboard,
|
| 269 |
+
outputs=[status, as_of, a1, a2, a3, chart, ladder_df, irr_df],
|
| 270 |
)
|
| 271 |
|
| 272 |
if __name__ == "__main__":
|