AshenH commited on
Commit
e521f8a
·
verified ·
1 Parent(s): 7a5eb22

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +40 -27
app.py CHANGED
@@ -1,6 +1,5 @@
1
  import os
2
  import sys
3
- from datetime import datetime
4
  from pathlib import Path
5
  from typing import Tuple, Any, List
6
 
@@ -9,7 +8,7 @@ import pandas as pd
9
  import numpy as np
10
  import matplotlib.pyplot as plt
11
  import gradio as gr
12
- from pydantic import BaseModel
13
  from reportlab.lib.pagesizes import A4
14
  from reportlab.lib.units import mm
15
  from reportlab.pdfgen import canvas
@@ -18,8 +17,8 @@ from reportlab.pdfgen import canvas
18
  # Basic configuration
19
  # -------------------------------------------------------------------
20
  APP_TITLE = "ALCO Liquidity & Interest-Rate Risk Dashboard"
21
- TABLE_FQN = "my_db.main.masterdataset_v"
22
- VIEW_FQN = "my_db.main.positions_v"
23
  EXPORT_DIR = Path("exports")
24
  EXPORT_DIR.mkdir(exist_ok=True)
25
 
@@ -42,10 +41,12 @@ def connect_md() -> duckdb.DuckDBPyConnection:
42
  # Column discovery & dynamic SQL
43
  # -------------------------------------------------------------------
44
  PRODUCT_ASSETS = [
45
- "assets"
 
46
  ]
47
  PRODUCT_SOF = [
48
- "fd"
 
49
  ]
50
 
51
  def discover_columns(conn: duckdb.DuckDBPyConnection, table_fqn: str) -> List[str]:
@@ -58,7 +59,6 @@ def discover_columns(conn: duckdb.DuckDBPyConnection, table_fqn: str) -> List[st
58
  df = conn.execute(q).fetchdf()
59
  return df["col"].tolist()
60
 
61
-
62
  def build_view_sql(existing_cols: List[str]) -> str:
63
  wanted = [
64
  "as_of_date", "product", "months", "segments",
@@ -70,6 +70,7 @@ def build_view_sql(existing_cols: List[str]) -> str:
70
  if c.lower() in existing_cols:
71
  select_list.append(c)
72
  else:
 
73
  if c in ("Portfolio_value", "Interest_rate", "days_to_maturity", "months"):
74
  select_list.append(f"CAST(NULL AS DOUBLE) AS {c}")
75
  else:
@@ -95,40 +96,46 @@ def build_view_sql(existing_cols: List[str]) -> str:
95
 
96
 
97
  # -------------------------------------------------------------------
98
- # Data model
99
  # -------------------------------------------------------------------
100
  class DashboardResult(BaseModel):
 
 
101
  as_of_date: str
102
  assets_t1: float
103
  sof_t1: float
104
  net_gap_t1: float
105
- ladder: pd.DataFrame
106
- irr: pd.DataFrame
107
 
108
 
109
  # -------------------------------------------------------------------
110
  # Core logic
111
  # -------------------------------------------------------------------
112
  def ensure_view(conn: duckdb.DuckDBPyConnection, existing_cols: List[str]):
113
- if not {"product", "portfolio_value", "days_to_maturity"}.issubset(set(existing_cols)):
114
- raise RuntimeError("Missing required columns in source table.")
115
- sql = build_view_sql(existing_cols)
116
- conn.execute(sql)
117
-
 
 
 
118
 
119
  def fetch_all(conn: duckdb.DuckDBPyConnection) -> DashboardResult:
120
  cols = discover_columns(conn, TABLE_FQN)
121
  ensure_view(conn, cols)
 
122
  has_asof = "as_of_date" in cols
123
  has_ir = "interest_rate" in cols
124
  has_months = "months" in cols
125
 
126
- # As-of date
127
- as_of_date = "N/A"
128
  if has_asof:
129
  asof_df = conn.execute(f"SELECT max(as_of_date) AS d FROM {VIEW_FQN}").fetchdf()
130
- if not asof_df.empty and not pd.isna(asof_df["d"].iloc[0]):
131
- as_of_date = str(asof_df["d"].iloc[0])[:10]
 
132
 
133
  # KPIs
134
  kpi_sql = f"""
@@ -158,19 +165,19 @@ def fetch_all(conn: duckdb.DuckDBPyConnection) -> DashboardResult:
158
  """
159
  ladder = conn.execute(ladder_sql).fetchdf()
160
 
161
- # IRR simplified
162
  t_expr = "CASE WHEN days_to_maturity IS NOT NULL THEN days_to_maturity/365.0"
163
  if has_months:
164
  t_expr += " WHEN months IS NOT NULL THEN months/12.0"
165
  t_expr += " ELSE NULL END"
166
- y_expr = "(Interest_rate/100.0)" if has_ir else "NULL"
167
 
168
  irr_sql = f"""
169
  SELECT
170
  bucket,
171
  SUM(Portfolio_value) AS pv_sum,
172
  SUM(Portfolio_value * {t_expr}) / NULLIF(SUM(Portfolio_value),0) AS dur_mac,
173
- SUM(Portfolio_value * ({t_expr})/(1+COALESCE({y_expr},0))) / NULLIF(SUM(Portfolio_value),0) AS dur_mod
174
  FROM {VIEW_FQN}
175
  GROUP BY bucket;
176
  """
@@ -189,13 +196,20 @@ def fetch_all(conn: duckdb.DuckDBPyConnection) -> DashboardResult:
189
  # -------------------------------------------------------------------
190
  # Visualization
191
  # -------------------------------------------------------------------
 
 
 
192
  def plot_ladder(df: pd.DataFrame):
193
  pivot = df.pivot(index="time_bucket", columns="bucket", values="amount").fillna(0)
194
  order = ["T+1", "T+2..7", "T+8..30", "T+31+"]
195
  pivot = pivot.reindex(order)
196
  fig, ax = plt.subplots(figsize=(7, 4))
197
- ax.bar(pivot.index, pivot.get("Assets", 0), label="Assets")
198
- ax.bar(pivot.index, -pivot.get("SoF", 0), label="SoF")
 
 
 
 
199
  ax.axhline(0, color="gray", lw=1)
200
  ax.set_ylabel("LKR")
201
  ax.set_title("Maturity Ladder (Assets vs SoF)")
@@ -228,7 +242,7 @@ def run_dashboard():
228
  conn = connect_md()
229
  res = fetch_all(conn)
230
  fig = plot_ladder(res.ladder)
231
- excel_path = export_excel(res)
232
  return (
233
  res.as_of_date,
234
  res.assets_t1,
@@ -237,10 +251,9 @@ def run_dashboard():
237
  fig,
238
  res.ladder,
239
  res.irr,
240
- str(excel_path),
241
  )
242
 
243
-
244
  with gr.Blocks(title=APP_TITLE) as demo:
245
  gr.Markdown(f"# {APP_TITLE}\n_Source:_ `{TABLE_FQN}` → `{VIEW_FQN}`")
246
 
 
1
  import os
2
  import sys
 
3
  from pathlib import Path
4
  from typing import Tuple, Any, List
5
 
 
8
  import numpy as np
9
  import matplotlib.pyplot as plt
10
  import gradio as gr
11
+ from pydantic import BaseModel, ConfigDict
12
  from reportlab.lib.pagesizes import A4
13
  from reportlab.lib.units import mm
14
  from reportlab.pdfgen import canvas
 
17
  # Basic configuration
18
  # -------------------------------------------------------------------
19
  APP_TITLE = "ALCO Liquidity & Interest-Rate Risk Dashboard"
20
+ TABLE_FQN = "my_db.main.masterdataset_v" # your source table
21
+ VIEW_FQN = "my_db.main.positions_v" # normalized view created by this app
22
  EXPORT_DIR = Path("exports")
23
  EXPORT_DIR.mkdir(exist_ok=True)
24
 
 
41
  # Column discovery & dynamic SQL
42
  # -------------------------------------------------------------------
43
  PRODUCT_ASSETS = [
44
+ "loan", "overdraft", "advances", "bills", "bill",
45
+ "tbond", "t-bond", "tbill", "t-bill", "repo_asset"
46
  ]
47
  PRODUCT_SOF = [
48
+ "fd", "term_deposit", "td", "savings", "current",
49
+ "call", "repo_liab"
50
  ]
51
 
52
  def discover_columns(conn: duckdb.DuckDBPyConnection, table_fqn: str) -> List[str]:
 
59
  df = conn.execute(q).fetchdf()
60
  return df["col"].tolist()
61
 
 
62
  def build_view_sql(existing_cols: List[str]) -> str:
63
  wanted = [
64
  "as_of_date", "product", "months", "segments",
 
70
  if c.lower() in existing_cols:
71
  select_list.append(c)
72
  else:
73
+ # fill missing columns with NULLs (typed)
74
  if c in ("Portfolio_value", "Interest_rate", "days_to_maturity", "months"):
75
  select_list.append(f"CAST(NULL AS DOUBLE) AS {c}")
76
  else:
 
96
 
97
 
98
  # -------------------------------------------------------------------
99
+ # Data model (allow pandas types)
100
  # -------------------------------------------------------------------
101
  class DashboardResult(BaseModel):
102
+ model_config = ConfigDict(arbitrary_types_allowed=True)
103
+
104
  as_of_date: str
105
  assets_t1: float
106
  sof_t1: float
107
  net_gap_t1: float
108
+ ladder: Any # pandas.DataFrame
109
+ irr: Any # pandas.DataFrame
110
 
111
 
112
  # -------------------------------------------------------------------
113
  # Core logic
114
  # -------------------------------------------------------------------
115
  def ensure_view(conn: duckdb.DuckDBPyConnection, existing_cols: List[str]):
116
+ # Mandatory columns in source:
117
+ required = {"product", "portfolio_value", "days_to_maturity"}
118
+ if not required.issubset(set(existing_cols)):
119
+ raise RuntimeError(
120
+ f"Source table {TABLE_FQN} must contain {sorted(required)}; "
121
+ f"found: {sorted(existing_cols)}"
122
+ )
123
+ conn.execute(build_view_sql(existing_cols))
124
 
125
  def fetch_all(conn: duckdb.DuckDBPyConnection) -> DashboardResult:
126
  cols = discover_columns(conn, TABLE_FQN)
127
  ensure_view(conn, cols)
128
+
129
  has_asof = "as_of_date" in cols
130
  has_ir = "interest_rate" in cols
131
  has_months = "months" in cols
132
 
133
+ # As-of date or N/A
 
134
  if has_asof:
135
  asof_df = conn.execute(f"SELECT max(as_of_date) AS d FROM {VIEW_FQN}").fetchdf()
136
+ as_of_date = "N/A" if asof_df.empty or pd.isna(asof_df["d"].iloc[0]) else str(asof_df["d"].iloc[0])[:10]
137
+ else:
138
+ as_of_date = "N/A"
139
 
140
  # KPIs
141
  kpi_sql = f"""
 
165
  """
166
  ladder = conn.execute(ladder_sql).fetchdf()
167
 
168
+ # IRR (approx) — works with or without months/interest_rate
169
  t_expr = "CASE WHEN days_to_maturity IS NOT NULL THEN days_to_maturity/365.0"
170
  if has_months:
171
  t_expr += " WHEN months IS NOT NULL THEN months/12.0"
172
  t_expr += " ELSE NULL END"
173
+ y_expr = "(Interest_rate/100.0)" if has_ir else "0.0"
174
 
175
  irr_sql = f"""
176
  SELECT
177
  bucket,
178
  SUM(Portfolio_value) AS pv_sum,
179
  SUM(Portfolio_value * {t_expr}) / NULLIF(SUM(Portfolio_value),0) AS dur_mac,
180
+ SUM(Portfolio_value * ({t_expr})/(1+({y_expr}))) / NULLIF(SUM(Portfolio_value),0) AS dur_mod
181
  FROM {VIEW_FQN}
182
  GROUP BY bucket;
183
  """
 
196
  # -------------------------------------------------------------------
197
  # Visualization
198
  # -------------------------------------------------------------------
199
+ def _zeros_like_index(index) -> pd.Series:
200
+ return pd.Series([0] * len(index), index=index)
201
+
202
  def plot_ladder(df: pd.DataFrame):
203
  pivot = df.pivot(index="time_bucket", columns="bucket", values="amount").fillna(0)
204
  order = ["T+1", "T+2..7", "T+8..30", "T+31+"]
205
  pivot = pivot.reindex(order)
206
  fig, ax = plt.subplots(figsize=(7, 4))
207
+
208
+ assets = pivot["Assets"] if "Assets" in pivot.columns else _zeros_like_index(pivot.index)
209
+ sof = pivot["SoF"] if "SoF" in pivot.columns else _zeros_like_index(pivot.index)
210
+
211
+ ax.bar(pivot.index, assets, label="Assets")
212
+ ax.bar(pivot.index, -sof, label="SoF")
213
  ax.axhline(0, color="gray", lw=1)
214
  ax.set_ylabel("LKR")
215
  ax.set_title("Maturity Ladder (Assets vs SoF)")
 
242
  conn = connect_md()
243
  res = fetch_all(conn)
244
  fig = plot_ladder(res.ladder)
245
+ xlsx = export_excel(res)
246
  return (
247
  res.as_of_date,
248
  res.assets_t1,
 
251
  fig,
252
  res.ladder,
253
  res.irr,
254
+ str(xlsx),
255
  )
256
 
 
257
  with gr.Blocks(title=APP_TITLE) as demo:
258
  gr.Markdown(f"# {APP_TITLE}\n_Source:_ `{TABLE_FQN}` → `{VIEW_FQN}`")
259