AshenH commited on
Commit
4bcc686
Β·
verified Β·
1 Parent(s): ef144b9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +117 -144
app.py CHANGED
@@ -1,13 +1,14 @@
1
  import os
2
  import sys
 
3
  from pathlib import Path
4
- from typing import Tuple, Any, List
5
 
6
  import duckdb
7
  import pandas as pd
8
  import numpy as np
9
  import matplotlib
10
- matplotlib.use("Agg") # headless backend for Spaces
11
  import matplotlib.pyplot as plt
12
  import gradio as gr
13
 
@@ -15,7 +16,7 @@ import gradio as gr
15
  # Basic configuration
16
  # =========================
17
  APP_TITLE = "ALCO Liquidity & Interest-Rate Risk Dashboard"
18
- TABLE_FQN = "my_db.main.masterdataset_v" # your source table
19
  VIEW_FQN = "my_db.main.positions_v" # normalized view created by this app
20
  EXPORT_DIR = Path("exports")
21
  EXPORT_DIR.mkdir(exist_ok=True)
@@ -32,35 +33,18 @@ PRODUCT_SOF = [
32
  # =========================
33
  # Helpers
34
  # =========================
35
- def safe_float(x, default: float = 0.0) -> float:
36
- try:
37
- if x is None or (isinstance(x, float) and np.isnan(x)):
38
- return default
39
- return float(x)
40
- except Exception:
41
- return default
42
-
43
- def zeros_like_index(index) -> pd.Series:
44
- return pd.Series([0] * len(index), index=index)
45
-
46
  def connect_md() -> duckdb.DuckDBPyConnection:
47
  token = os.environ.get("MOTHERDUCK_TOKEN", "")
48
  if not token:
49
- raise RuntimeError("MOTHERDUCK_TOKEN is not set. Add it in your Space β†’ Settings β†’ Secrets.")
50
- try:
51
- return duckdb.connect(f"md:?motherduck_token={token}")
52
- except Exception as e:
53
- raise RuntimeError(f"MotherDuck connection failed: {e}") from e
54
 
55
  def discover_columns(conn: duckdb.DuckDBPyConnection, table_fqn: str) -> List[str]:
56
- q = f"""
57
- SELECT lower(column_name) AS col
58
- FROM information_schema.columns
59
- WHERE table_schema = split_part('{table_fqn}', '.', 2)
60
- AND table_name = split_part('{table_fqn}', '.', 3)
61
- """
62
- df = conn.execute(q).fetchdf()
63
- return df["col"].tolist()
64
 
65
  def build_view_sql(existing_cols: List[str]) -> str:
66
  wanted = [
@@ -68,28 +52,24 @@ def build_view_sql(existing_cols: List[str]) -> str:
68
  "currency", "Portfolio_value", "Interest_rate",
69
  "days_to_maturity"
70
  ]
71
- select_list = []
72
  for c in wanted:
73
  if c.lower() in existing_cols:
74
- select_list.append(c)
75
  else:
76
- # fill missing columns with typed NULLs
77
  if c in ("Portfolio_value", "Interest_rate", "days_to_maturity", "months"):
78
- select_list.append(f"CAST(NULL AS DOUBLE) AS {c}")
79
  else:
80
- select_list.append(f"CAST(NULL AS VARCHAR) AS {c}")
81
 
82
  sof_list = ", ".join([f"'{p}'" for p in PRODUCT_SOF])
83
  asset_list = ", ".join([f"'{p}'" for p in PRODUCT_ASSETS])
84
-
85
  bucket_case = (
86
- f"CASE "
87
- f"WHEN lower(product) IN ({sof_list}) THEN 'SoF' "
88
  f"WHEN lower(product) IN ({asset_list}) THEN 'Assets' "
89
  f"ELSE 'Unknown' END AS bucket"
90
  )
91
-
92
- select_sql = ",\n ".join(select_list + [bucket_case])
93
  return f"""
94
  CREATE OR REPLACE VIEW {VIEW_FQN} AS
95
  SELECT
@@ -97,87 +77,36 @@ def build_view_sql(existing_cols: List[str]) -> str:
97
  FROM {TABLE_FQN};
98
  """
99
 
100
- def ensure_view(conn: duckdb.DuckDBPyConnection, existing_cols: List[str]):
101
  required = {"product", "portfolio_value", "days_to_maturity"}
102
- if not required.issubset(set(existing_cols)):
103
  raise RuntimeError(
104
- f"Source table {TABLE_FQN} must contain columns {sorted(required)}; "
105
- f"found {sorted(existing_cols)}"
106
  )
107
- conn.execute(build_view_sql(existing_cols))
108
-
109
- def fetch_kpis(conn: duckdb.DuckDBPyConnection) -> Tuple[float, float, float]:
110
- sql = f"""
111
- SELECT
112
- COALESCE(SUM(CASE WHEN bucket='Assets' AND days_to_maturity<=1 THEN Portfolio_value END),0) AS assets_t1,
113
- COALESCE(SUM(CASE WHEN bucket='SoF' AND days_to_maturity<=1 THEN Portfolio_value END),0) AS sof_t1,
114
- COALESCE(SUM(CASE WHEN bucket='Assets' AND days_to_maturity<=1 THEN Portfolio_value END),0)
115
- - COALESCE(SUM(CASE WHEN bucket='SoF' AND days_to_maturity<=1 THEN Portfolio_value END),0) AS net_gap_t1
116
- FROM {VIEW_FQN};
117
- """
118
- df = conn.execute(sql).fetchdf()
119
- if df.empty:
120
- return 0.0, 0.0, 0.0
121
- return safe_float(df["assets_t1"].iloc[0]), safe_float(df["sof_t1"].iloc[0]), safe_float(df["net_gap_t1"].iloc[0])
122
 
123
- def fetch_ladder(conn: duckdb.DuckDBPyConnection) -> pd.DataFrame:
124
- sql = f"""
125
- SELECT
126
- CASE
127
- WHEN days_to_maturity <= 1 THEN 'T+1'
128
- WHEN days_to_maturity BETWEEN 2 AND 7 THEN 'T+2..7'
129
- WHEN days_to_maturity BETWEEN 8 AND 30 THEN 'T+8..30'
130
- ELSE 'T+31+'
131
- END AS time_bucket,
132
- bucket,
133
- SUM(Portfolio_value) AS amount
134
- FROM {VIEW_FQN}
135
- GROUP BY 1,2
136
- ORDER BY 1,2;
137
- """
138
- df = conn.execute(sql).fetchdf()
139
- if df.empty:
140
- return pd.DataFrame({"time_bucket": [], "bucket": [], "amount": []})
141
- return df
142
 
143
- def fetch_irr(conn: duckdb.DuckDBPyConnection, cols: List[str]) -> pd.DataFrame:
144
- has_months = "months" in cols
145
- has_ir = "interest_rate" in cols
146
- t_expr = "CASE WHEN days_to_maturity IS NOT NULL THEN days_to_maturity/365.0"
147
- if has_months:
148
- t_expr += " WHEN months IS NOT NULL THEN months/12.0"
149
- t_expr += " ELSE NULL END"
150
- y_expr = "(Interest_rate/100.0)" if has_ir else "0.0"
151
- sql = f"""
152
- SELECT
153
- bucket,
154
- SUM(Portfolio_value) AS pv_sum,
155
- SUM(Portfolio_value * {t_expr}) / NULLIF(SUM(Portfolio_value),0) AS dur_mac,
156
- SUM(Portfolio_value * ({t_expr})/(1+({y_expr}))) / NULLIF(SUM(Portfolio_value),0) AS dur_mod
157
- FROM {VIEW_FQN}
158
- GROUP BY bucket;
159
- """
160
- df = conn.execute(sql).fetchdf()
161
- if df.empty:
162
- return pd.DataFrame({"bucket": [], "pv_sum": [], "dur_mac": [], "dur_mod": []})
163
- return df
164
 
165
  def plot_ladder(df: pd.DataFrame):
166
  try:
167
- if df.empty:
168
  fig, ax = plt.subplots(figsize=(7, 3))
169
- ax.text(0.5, 0.5, "No data", ha="center", va="center", fontsize=12)
170
  ax.axis("off")
171
  return fig
172
-
173
  pivot = df.pivot(index="time_bucket", columns="bucket", values="amount").fillna(0)
174
  order = ["T+1", "T+2..7", "T+8..30", "T+31+"]
175
  pivot = pivot.reindex(order)
176
  fig, ax = plt.subplots(figsize=(7, 4))
177
-
178
  assets = pivot["Assets"] if "Assets" in pivot.columns else zeros_like_index(pivot.index)
179
  sof = pivot["SoF"] if "SoF" in pivot.columns else zeros_like_index(pivot.index)
180
-
181
  ax.bar(pivot.index, assets, label="Assets")
182
  ax.bar(pivot.index, -sof, label="SoF")
183
  ax.axhline(0, color="gray", lw=1)
@@ -187,89 +116,132 @@ def plot_ladder(df: pd.DataFrame):
187
  fig.tight_layout()
188
  return fig
189
  except Exception as e:
190
- # Return a simple figure with the error rendered
191
  fig, ax = plt.subplots(figsize=(7, 3))
192
  ax.text(0.01, 0.8, "Chart Error:", fontsize=12, ha="left")
193
  ax.text(0.01, 0.5, str(e), fontsize=10, ha="left", wrap=True)
194
  ax.axis("off")
195
  return fig
196
 
197
- def export_excel(as_of_date: str,
198
- assets_t1: float,
199
- sof_t1: float,
200
- net_gap_t1: float,
201
- ladder: pd.DataFrame,
202
- irr: pd.DataFrame) -> Path:
203
- out = EXPORT_DIR / f"alco_report_{as_of_date}.xlsx"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
  with pd.ExcelWriter(out, engine="xlsxwriter") as xw:
205
  pd.DataFrame({
206
- "as_of_date": [as_of_date],
207
  "assets_t1": [assets_t1],
208
  "sof_t1": [sof_t1],
209
  "net_gap_t1": [net_gap_t1],
210
  }).to_excel(xw, index=False, sheet_name="kpis")
211
- ladder.to_excel(xw, index=False, sheet_name="ladder")
212
- irr.to_excel(xw, index=False, sheet_name="irr")
213
  return out
214
 
215
  # =========================
216
- # Gradio UI logic
217
  # =========================
218
- def run_dashboard():
219
  """
220
  Returns:
221
- status (str),
222
- as_of (str),
223
- assets_t1 (float),
224
- sof_t1 (float),
225
- net_gap_t1 (float),
226
- fig (matplotlib fig),
227
- ladder_df (DataFrame),
228
- irr_df (DataFrame),
229
- excel_file (path str)
230
  """
231
- status = "βœ… OK"
232
  try:
233
  conn = connect_md()
234
- cols = discover_columns(conn, TABLE_FQN) # lower-cased names
 
 
235
  ensure_view(conn, cols)
236
 
237
- # As-of when available (otherwise N/A)
238
  as_of = "N/A"
239
  if "as_of_date" in cols:
240
  tmp = conn.execute(f"SELECT max(as_of_date) AS d FROM {VIEW_FQN}").fetchdf()
241
  if not tmp.empty and not pd.isna(tmp["d"].iloc[0]):
242
  as_of = str(tmp["d"].iloc[0])[:10]
243
 
244
- assets_t1, sof_t1, net_gap_t1 = fetch_kpis(conn)
245
- ladder = fetch_ladder(conn)
246
- irr = fetch_irr(conn, cols)
 
 
 
 
 
 
247
 
248
  fig = plot_ladder(ladder)
249
- xlsx_path = export_excel(as_of, assets_t1, sof_t1, net_gap_t1, ladder, irr)
 
250
 
 
251
  return (
252
  status,
253
  as_of,
254
- assets_t1,
255
- sof_t1,
256
- net_gap_t1,
257
  fig,
258
  ladder,
259
  irr,
260
- str(xlsx_path),
261
  )
 
262
  except Exception as e:
263
- # Swallow the error for the UI; show user-friendly message + zeros/empty placeholders
264
- status = f"❌ Error: {e}"
265
  empty_df = pd.DataFrame()
266
  fig = plot_ladder(empty_df)
267
  return (
268
- status,
269
  "N/A",
270
- 0.0,
271
- 0.0,
272
- 0.0,
273
  fig,
274
  empty_df,
275
  empty_df,
@@ -282,17 +254,18 @@ def run_dashboard():
282
  with gr.Blocks(title=APP_TITLE) as demo:
283
  gr.Markdown(f"# {APP_TITLE}\n_Source:_ `{TABLE_FQN}` β†’ `{VIEW_FQN}`")
284
 
285
- status = gr.Textbox(label="Status", interactive=False)
286
 
287
  with gr.Row():
288
  refresh_btn = gr.Button("πŸ”„ Refresh", variant="primary")
289
 
290
  with gr.Row():
291
  as_of = gr.Textbox(label="As of date", interactive=False)
 
292
  with gr.Row():
293
- a1 = gr.Number(label="Assets T+1 (LKR)", precision=0)
294
- a2 = gr.Number(label="SoF T+1 (LKR)", precision=0)
295
- a3 = gr.Number(label="Net Gap T+1 (LKR)", precision=0)
296
 
297
  chart = gr.Plot(label="Maturity Ladder")
298
  ladder_df = gr.Dataframe(label="Ladder Detail")
 
1
  import os
2
  import sys
3
+ import traceback
4
  from pathlib import Path
5
+ from typing import List, Tuple, Any
6
 
7
  import duckdb
8
  import pandas as pd
9
  import numpy as np
10
  import matplotlib
11
+ matplotlib.use("Agg") # headless for Spaces
12
  import matplotlib.pyplot as plt
13
  import gradio as gr
14
 
 
16
  # Basic configuration
17
  # =========================
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)
 
33
  # =========================
34
  # Helpers
35
  # =========================
 
 
 
 
 
 
 
 
 
 
 
36
  def connect_md() -> duckdb.DuckDBPyConnection:
37
  token = os.environ.get("MOTHERDUCK_TOKEN", "")
38
  if not token:
39
+ raise RuntimeError("MOTHERDUCK_TOKEN is not set. Add it in Space β†’ Settings β†’ Secrets.")
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 = [
 
52
  "currency", "Portfolio_value", "Interest_rate",
53
  "days_to_maturity"
54
  ]
55
+ sel = []
56
  for c in wanted:
57
  if c.lower() in existing_cols:
58
+ sel.append(c)
59
  else:
 
60
  if c in ("Portfolio_value", "Interest_rate", "days_to_maturity", "months"):
61
+ sel.append(f"CAST(NULL AS DOUBLE) AS {c}")
62
  else:
63
+ sel.append(f"CAST(NULL AS VARCHAR) AS {c}")
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
  )
72
+ select_sql = ",\n ".join(sel + [bucket_case])
 
73
  return f"""
74
  CREATE OR REPLACE VIEW {VIEW_FQN} AS
75
  SELECT
 
77
  FROM {TABLE_FQN};
78
  """
79
 
80
+ def ensure_view(conn: duckdb.DuckDBPyConnection, cols: List[str]) -> None:
81
  required = {"product", "portfolio_value", "days_to_maturity"}
82
+ if not required.issubset(set(cols)):
83
  raise RuntimeError(
84
+ f"Source table {TABLE_FQN} must contain columns {sorted(required)}; found {sorted(cols)}"
 
85
  )
86
+ conn.execute(build_view_sql(cols))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
 
88
+ def safe_num(x) -> float:
89
+ try:
90
+ return float(0.0 if x is None or (isinstance(x, float) and np.isnan(x)) else x)
91
+ except Exception:
92
+ return 0.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
 
94
+ def zeros_like_index(index) -> pd.Series:
95
+ return pd.Series([0] * len(index), index=index)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
 
97
  def plot_ladder(df: pd.DataFrame):
98
  try:
99
+ if df is None or df.empty:
100
  fig, ax = plt.subplots(figsize=(7, 3))
101
+ ax.text(0.5, 0.5, "No data", ha="center", va="center")
102
  ax.axis("off")
103
  return fig
 
104
  pivot = df.pivot(index="time_bucket", columns="bucket", values="amount").fillna(0)
105
  order = ["T+1", "T+2..7", "T+8..30", "T+31+"]
106
  pivot = pivot.reindex(order)
107
  fig, ax = plt.subplots(figsize=(7, 4))
 
108
  assets = pivot["Assets"] if "Assets" in pivot.columns else zeros_like_index(pivot.index)
109
  sof = pivot["SoF"] if "SoF" in pivot.columns else zeros_like_index(pivot.index)
 
110
  ax.bar(pivot.index, assets, label="Assets")
111
  ax.bar(pivot.index, -sof, label="SoF")
112
  ax.axhline(0, color="gray", lw=1)
 
116
  fig.tight_layout()
117
  return fig
118
  except Exception as e:
 
119
  fig, ax = plt.subplots(figsize=(7, 3))
120
  ax.text(0.01, 0.8, "Chart Error:", fontsize=12, ha="left")
121
  ax.text(0.01, 0.5, str(e), fontsize=10, ha="left", wrap=True)
122
  ax.axis("off")
123
  return fig
124
 
125
+ # =========================
126
+ # Query fragments
127
+ # =========================
128
+ KPI_SQL = f"""
129
+ SELECT
130
+ COALESCE(SUM(CASE WHEN bucket='Assets' AND days_to_maturity<=1 THEN Portfolio_value END),0) AS assets_t1,
131
+ COALESCE(SUM(CASE WHEN bucket='SoF' AND days_to_maturity<=1 THEN Portfolio_value END),0) AS sof_t1,
132
+ COALESCE(SUM(CASE WHEN bucket='Assets' AND days_to_maturity<=1 THEN Portfolio_value END),0)
133
+ - COALESCE(SUM(CASE WHEN bucket='SoF' AND days_to_maturity<=1 THEN Portfolio_value END),0) AS net_gap_t1
134
+ FROM {VIEW_FQN};
135
+ """
136
+
137
+ LADDER_SQL = f"""
138
+ SELECT
139
+ CASE
140
+ WHEN days_to_maturity <= 1 THEN 'T+1'
141
+ WHEN days_to_maturity BETWEEN 2 AND 7 THEN 'T+2..7'
142
+ WHEN days_to_maturity BETWEEN 8 AND 30 THEN 'T+8..30'
143
+ ELSE 'T+31+'
144
+ END AS time_bucket,
145
+ bucket,
146
+ SUM(Portfolio_value) AS amount
147
+ FROM {VIEW_FQN}
148
+ GROUP BY 1,2
149
+ ORDER BY 1,2;
150
+ """
151
+
152
+ def irr_sql(cols: List[str]) -> str:
153
+ has_months = "months" in cols
154
+ has_ir = "interest_rate" in cols
155
+ t_expr = "CASE WHEN days_to_maturity IS NOT NULL THEN days_to_maturity/365.0"
156
+ if has_months:
157
+ t_expr += " WHEN months IS NOT NULL THEN months/12.0"
158
+ t_expr += " ELSE NULL END"
159
+ y_expr = "(Interest_rate/100.0)" if has_ir else "0.0"
160
+ return f"""
161
+ SELECT
162
+ bucket,
163
+ SUM(Portfolio_value) AS pv_sum,
164
+ SUM(Portfolio_value * {t_expr}) / NULLIF(SUM(Portfolio_value),0) AS dur_mac,
165
+ SUM(Portfolio_value * ({t_expr})/(1+({y_expr}))) / NULLIF(SUM(Portfolio_value),0) AS dur_mod
166
+ FROM {VIEW_FQN}
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 (
223
  status,
224
  as_of,
225
+ f"{assets_t1:,.0f}",
226
+ f"{sof_t1:,.0f}",
227
+ f"{net_gap:,.0f}",
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 (
240
+ f"❌ Error: {e}\n\n{tb}",
241
  "N/A",
242
+ "0",
243
+ "0",
244
+ "0",
245
  fig,
246
  empty_df,
247
  empty_df,
 
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")
261
 
262
  with gr.Row():
263
  as_of = gr.Textbox(label="As of date", interactive=False)
264
+
265
  with gr.Row():
266
+ a1 = gr.Textbox(label="Assets T+1 (LKR)", interactive=False)
267
+ a2 = gr.Textbox(label="SoF T+1 (LKR)", interactive=False)
268
+ a3 = gr.Textbox(label="Net Gap T+1 (LKR)", interactive=False)
269
 
270
  chart = gr.Plot(label="Maturity Ladder")
271
  ladder_df = gr.Dataframe(label="Ladder Detail")