AshenH commited on
Commit
36baf26
Β·
verified Β·
1 Parent(s): cd7c6ea

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +30 -38
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", "assets"
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
- # More robust than information_schema across DuckDB/MotherDuck
44
- df = conn.execute(f"DESCRIBE {table_fqn};").fetchdf()
45
- # DuckDB: columns listed under 'column_name'
46
- name_col = "column_name" if "column_name" in df.columns else df.columns[0]
47
- return [str(c).lower() for c in df[name_col].tolist()]
 
 
 
 
 
 
 
 
 
 
 
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 WHEN lower(product) IN ({sof_list}) THEN 'SoF' "
 
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, str]:
188
  """
189
  Returns:
190
- status, as_of, assets_t1, sof_t1, net_gap_t1, figure, ladder_df, irr_df, excel_path
191
- (all text values are returned as strings to avoid component type errors)
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
- # as_of (optional)
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=5)
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, excel_file],
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__":