Update app.py
Browse files
app.py
CHANGED
|
@@ -112,11 +112,9 @@ def plot_ladder(df: pd.DataFrame):
|
|
| 112 |
ax.text(0.5, 0.5, "No data", ha="center", va="center")
|
| 113 |
ax.axis("off")
|
| 114 |
return fig
|
| 115 |
-
pivot = df.pivot(index="time_bucket", columns="bucket", values="
|
| 116 |
order = ["T+1", "T+2..7", "T+8..30", "T+31+"]
|
| 117 |
pivot = pivot.reindex(order)
|
| 118 |
-
# Convert to millions for plotting
|
| 119 |
-
pivot = pivot / 1_000_000
|
| 120 |
fig, ax = plt.subplots(figsize=(7, 4))
|
| 121 |
assets = pivot["Assets"] if "Assets" in pivot.columns else zeros_like_index(pivot.index)
|
| 122 |
sof = pivot["SoF"] if "SoF" in pivot.columns else zeros_like_index(pivot.index)
|
|
@@ -162,6 +160,17 @@ GROUP BY 1,2
|
|
| 162 |
ORDER BY 1,2;
|
| 163 |
"""
|
| 164 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
def irr_sql(cols: List[str]) -> str:
|
| 166 |
has_months = "months" in cols
|
| 167 |
has_ir = "interest_rate" in cols
|
|
@@ -181,11 +190,11 @@ def irr_sql(cols: List[str]) -> str:
|
|
| 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,
|
| 188 |
-
|
| 189 |
"""
|
| 190 |
try:
|
| 191 |
conn = connect_md()
|
|
@@ -207,24 +216,55 @@ def run_dashboard() -> Tuple[str, str, str, str, str, Any, pd.DataFrame, pd.Data
|
|
| 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
|
| 211 |
ladder = conn.execute(LADDER_SQL).fetchdf()
|
| 212 |
irr = conn.execute(irr_sql(cols)).fetchdf()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
if "Portfolio Value (LKR Mn)" in irr.columns:
|
| 214 |
irr["Portfolio Value (LKR Mn)"] = irr["Portfolio Value (LKR Mn)"].map('{:,.2f}'.format)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 215 |
|
| 216 |
# 5) Chart
|
| 217 |
fig = plot_ladder(ladder)
|
| 218 |
|
|
|
|
| 219 |
assets_t1_mn_str = f"{(assets_t1 / 1_000_000):,.2f}"
|
| 220 |
sof_t1_mn_str = f"{(sof_t1 / 1_000_000):,.2f}"
|
| 221 |
net_gap_mn_str = f"{(net_gap / 1_000_000):,.2f}"
|
|
|
|
| 222 |
|
| 223 |
a1_text = f"The amount of Assets maturing tomorrow (T+1) is **LKR {assets_t1_mn_str} Mn**."
|
| 224 |
a2_text = f"The amount of Sources of Funds (SoF) maturing tomorrow (T+1) is **LKR {sof_t1_mn_str} Mn**."
|
| 225 |
a3_text = f"The resulting Net Liquidity Gap for tomorrow (T+1) is **LKR {net_gap_mn_str} Mn**."
|
| 226 |
|
| 227 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
return (
|
| 229 |
status,
|
| 230 |
as_of,
|
|
@@ -234,6 +274,8 @@ def run_dashboard() -> Tuple[str, str, str, str, str, Any, pd.DataFrame, pd.Data
|
|
| 234 |
fig,
|
| 235 |
ladder,
|
| 236 |
irr,
|
|
|
|
|
|
|
| 237 |
)
|
| 238 |
|
| 239 |
except Exception as e:
|
|
@@ -249,6 +291,8 @@ def run_dashboard() -> Tuple[str, str, str, str, str, Any, pd.DataFrame, pd.Data
|
|
| 249 |
fig,
|
| 250 |
empty_df,
|
| 251 |
empty_df,
|
|
|
|
|
|
|
| 252 |
)
|
| 253 |
|
| 254 |
# =========================
|
|
@@ -269,13 +313,21 @@ with gr.Blocks(title=APP_TITLE) as demo:
|
|
| 269 |
a2 = gr.Markdown("The amount of Sources of Funds (SoF) maturing tomorrow (T+1) is...")
|
| 270 |
a3 = gr.Markdown("The resulting Net Liquidity Gap for tomorrow (T+1) is...")
|
| 271 |
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 275 |
|
| 276 |
refresh_btn.click(
|
| 277 |
fn=run_dashboard,
|
| 278 |
-
outputs=[status, as_of, a1, a2, a3, chart, ladder_df, irr_df],
|
| 279 |
)
|
| 280 |
|
| 281 |
if __name__ == "__main__":
|
|
|
|
| 112 |
ax.text(0.5, 0.5, "No data", ha="center", va="center")
|
| 113 |
ax.axis("off")
|
| 114 |
return fig
|
| 115 |
+
pivot = df.pivot(index="time_bucket", columns="bucket", values="Amount (LKR Mn)").fillna(0)
|
| 116 |
order = ["T+1", "T+2..7", "T+8..30", "T+31+"]
|
| 117 |
pivot = pivot.reindex(order)
|
|
|
|
|
|
|
| 118 |
fig, ax = plt.subplots(figsize=(7, 4))
|
| 119 |
assets = pivot["Assets"] if "Assets" in pivot.columns else zeros_like_index(pivot.index)
|
| 120 |
sof = pivot["SoF"] if "SoF" in pivot.columns else zeros_like_index(pivot.index)
|
|
|
|
| 160 |
ORDER BY 1,2;
|
| 161 |
"""
|
| 162 |
|
| 163 |
+
GAP_DRIVERS_SQL = f"""
|
| 164 |
+
SELECT
|
| 165 |
+
product,
|
| 166 |
+
bucket,
|
| 167 |
+
SUM(Portfolio_value) / 1000000.0 AS "Amount (LKR Mn)"
|
| 168 |
+
FROM {VIEW_FQN}
|
| 169 |
+
WHERE days_to_maturity <= 1
|
| 170 |
+
GROUP BY 1, 2
|
| 171 |
+
ORDER BY 3 DESC;
|
| 172 |
+
"""
|
| 173 |
+
|
| 174 |
def irr_sql(cols: List[str]) -> str:
|
| 175 |
has_months = "months" in cols
|
| 176 |
has_ir = "interest_rate" in cols
|
|
|
|
| 190 |
# =========================
|
| 191 |
# Dashboard callback
|
| 192 |
# =========================
|
| 193 |
+
def run_dashboard() -> Tuple[str, str, str, str, str, Any, pd.DataFrame, pd.DataFrame, str, pd.DataFrame]:
|
| 194 |
"""
|
| 195 |
Returns:
|
| 196 |
+
status, as_of, a1_text, a2_text, a3_text, figure, ladder_df, irr_df,
|
| 197 |
+
explain_text, drivers_df
|
| 198 |
"""
|
| 199 |
try:
|
| 200 |
conn = connect_md()
|
|
|
|
| 216 |
sof_t1 = safe_num(kpi["sof_t1"].iloc[0]) if not kpi.empty else 0.0
|
| 217 |
net_gap = safe_num(kpi["net_gap_t1"].iloc[0]) if not kpi.empty else 0.0
|
| 218 |
|
| 219 |
+
# 4) Ladder, IRR, and Gap Drivers
|
| 220 |
ladder = conn.execute(LADDER_SQL).fetchdf()
|
| 221 |
irr = conn.execute(irr_sql(cols)).fetchdf()
|
| 222 |
+
drivers = conn.execute(GAP_DRIVERS_SQL).fetchdf()
|
| 223 |
+
|
| 224 |
+
if "Amount (LKR Mn)" in ladder.columns:
|
| 225 |
+
ladder["Amount (LKR Mn)"] = ladder["Amount (LKR Mn)"].map('{:,.2f}'.format)
|
| 226 |
if "Portfolio Value (LKR Mn)" in irr.columns:
|
| 227 |
irr["Portfolio Value (LKR Mn)"] = irr["Portfolio Value (LKR Mn)"].map('{:,.2f}'.format)
|
| 228 |
+
if "Amount (LKR Mn)" in drivers.columns:
|
| 229 |
+
drivers_display = drivers.copy()
|
| 230 |
+
drivers_display["Amount (LKR Mn)"] = drivers_display["Amount (LKR Mn)"].map('{:,.2f}'.format)
|
| 231 |
+
else:
|
| 232 |
+
drivers_display = pd.DataFrame()
|
| 233 |
|
| 234 |
# 5) Chart
|
| 235 |
fig = plot_ladder(ladder)
|
| 236 |
|
| 237 |
+
# 6) Explanations
|
| 238 |
assets_t1_mn_str = f"{(assets_t1 / 1_000_000):,.2f}"
|
| 239 |
sof_t1_mn_str = f"{(sof_t1 / 1_000_000):,.2f}"
|
| 240 |
net_gap_mn_str = f"{(net_gap / 1_000_000):,.2f}"
|
| 241 |
+
gap_sign_str = "positive" if net_gap >= 0 else "negative"
|
| 242 |
|
| 243 |
a1_text = f"The amount of Assets maturing tomorrow (T+1) is **LKR {assets_t1_mn_str} Mn**."
|
| 244 |
a2_text = f"The amount of Sources of Funds (SoF) maturing tomorrow (T+1) is **LKR {sof_t1_mn_str} Mn**."
|
| 245 |
a3_text = f"The resulting Net Liquidity Gap for tomorrow (T+1) is **LKR {net_gap_mn_str} Mn**."
|
| 246 |
|
| 247 |
+
# Build "Why" text
|
| 248 |
+
sof_drivers = drivers[drivers["bucket"] == "SoF"]
|
| 249 |
+
asset_drivers = drivers[drivers["bucket"] == "Assets"]
|
| 250 |
+
top_sof_prod = sof_drivers.iloc[0] if not sof_drivers.empty else None
|
| 251 |
+
top_asset_prod = asset_drivers.iloc[0] if not asset_drivers.empty else None
|
| 252 |
+
|
| 253 |
+
explain_text = f"### Why is the T+1 Gap {gap_sign_str}?\n\n"
|
| 254 |
+
if top_sof_prod is not None:
|
| 255 |
+
explain_text += f"* **Largest Liability Maturity:** The largest outflow comes from `{top_sof_prod['product']}`, with **LKR {top_sof_prod['Amount (LKR Mn)']:,.2f} Mn** maturing.\n"
|
| 256 |
+
else:
|
| 257 |
+
explain_text += "* **Largest Liability Maturity:** No significant liabilities are maturing tomorrow.\n"
|
| 258 |
+
|
| 259 |
+
if top_asset_prod is not None:
|
| 260 |
+
explain_text += f"* **Largest Asset Inflow:** The largest inflow comes from `{top_asset_prod['product']}`, with **LKR {top_asset_prod['Amount (LKR Mn)']:,.2f} Mn** maturing.\n"
|
| 261 |
+
else:
|
| 262 |
+
explain_text += "* **Largest Asset Inflow:** No significant assets are maturing to provide inflows tomorrow.\n"
|
| 263 |
+
|
| 264 |
+
# Note: The data source does not contain features for seasonal analysis (e.g., day_of_week, is_month_end).
|
| 265 |
+
explain_text += "* **Seasonal Pattern:** Analysis not possible without relevant time-series features in the source data."
|
| 266 |
+
|
| 267 |
+
status = f"✅ OK (as of {pd.Timestamp.now().strftime('%Y-%m-%d %H:%M:%S')})"
|
| 268 |
return (
|
| 269 |
status,
|
| 270 |
as_of,
|
|
|
|
| 274 |
fig,
|
| 275 |
ladder,
|
| 276 |
irr,
|
| 277 |
+
explain_text,
|
| 278 |
+
drivers_display,
|
| 279 |
)
|
| 280 |
|
| 281 |
except Exception as e:
|
|
|
|
| 291 |
fig,
|
| 292 |
empty_df,
|
| 293 |
empty_df,
|
| 294 |
+
"Analysis could not be performed.",
|
| 295 |
+
empty_df,
|
| 296 |
)
|
| 297 |
|
| 298 |
# =========================
|
|
|
|
| 313 |
a2 = gr.Markdown("The amount of Sources of Funds (SoF) maturing tomorrow (T+1) is...")
|
| 314 |
a3 = gr.Markdown("The resulting Net Liquidity Gap for tomorrow (T+1) is...")
|
| 315 |
|
| 316 |
+
with gr.Row():
|
| 317 |
+
with gr.Column(scale=2):
|
| 318 |
+
chart = gr.Plot(label="Maturity Ladder")
|
| 319 |
+
ladder_df = gr.Dataframe(label="Ladder Detail")
|
| 320 |
+
irr_df = gr.Dataframe(label="Interest-Rate Risk (approx)")
|
| 321 |
+
with gr.Column(scale=1):
|
| 322 |
+
explain_text = gr.Markdown("Analysis of the T+1 gap will appear here...")
|
| 323 |
+
drivers_df = gr.Dataframe(
|
| 324 |
+
label="T+1 Gap Drivers (Top Products)",
|
| 325 |
+
headers=["Product", "Bucket", "Amount (LKR Mn)"],
|
| 326 |
+
)
|
| 327 |
|
| 328 |
refresh_btn.click(
|
| 329 |
fn=run_dashboard,
|
| 330 |
+
outputs=[status, as_of, a1, a2, a3, chart, ladder_df, irr_df, explain_text, drivers_df],
|
| 331 |
)
|
| 332 |
|
| 333 |
if __name__ == "__main__":
|