AshenH commited on
Commit
78f9096
·
verified ·
1 Parent(s): 22cc556

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +199 -73
app.py CHANGED
@@ -1,87 +1,213 @@
1
  import os
2
- up = res.shocks.loc[res.shocks['bucket']==res.shocks['bucket'].unique()[0], 'dPV_up_100bp'].sum()
3
- dn = res.shocks.loc[res.shocks['bucket']==res.shocks['bucket'].unique()[0], 'dPV_dn_100bp'].sum()
4
- # Better: show net across buckets
5
- net_up = res.shocks['dPV_up_100bp'].sum()
6
- net_dn = res.shocks['dPV_dn_100bp'].sum()
7
- y -= 2*mm
8
- line(f"+100bp net ΔPV: {net_up:,.0f} LKR | -100bp net ΔPV: {net_dn:,.0f} LKR")
9
- c.showPage()
10
- c.save()
11
- return out
12
 
13
- # ---------- Gradio UI ----------
 
 
 
 
 
 
 
 
14
 
15
- def run_dashboard() -> Tuple[str, float, float, float, Any, Any, Any, Any, Any, Any, Any]:
16
- conn = connect_md()
17
- res = fetch_all(conn)
18
- fig = plot_ladder(res.ladder)
19
- excel_path = export_excel(res)
20
- pdf_path = export_pdf(res)
21
- return (
22
- res.as_of_date,
23
- res.assets_t1,
24
- res.sof_t1,
25
- res.net_gap_t1,
26
- fig,
27
- res.t1_by_month,
28
- res.t1_by_segment,
29
- res.t1_by_ccy,
30
- res.irr,
31
- res.shocks,
32
- str(excel_path),
33
- str(pdf_path),
34
- )
35
 
36
 
37
- with gr.Blocks(title=APP_TITLE) as demo:
38
- gr.Markdown(
39
- f"# {APP_TITLE}\n"
40
- "*Source:* `my_db.main.masterdataset_v` `positions_v` | *Sign:* Assets=+ SoF=–"
41
- )
 
 
 
 
42
 
43
- with gr.Row():
44
- btn = gr.Button("🔄 Refresh", variant="primary")
45
 
46
- with gr.Row():
47
- as_of = gr.Textbox(label="As of date", interactive=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
- with gr.Row():
50
- k1 = gr.Number(label="Assets T+1 (LKR)", precision=0)
51
- k2 = gr.Number(label="SoF T+1 (LKR)", precision=0)
52
- k3 = gr.Number(label="Net Gap T+1 (LKR)", precision=0)
53
 
54
- chart = gr.Plot(label="Maturity Ladder")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
- with gr.Row():
57
- t1m = gr.Dataframe(label="T+1 by Tenor (months)")
58
- t1s = gr.Dataframe(label="T+1 by Segment")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
 
60
- t1c = gr.Dataframe(label="T+1 by Currency")
61
- irr = gr.Dataframe(label="Interest-Rate Risk (bucketed)")
62
- shocks = gr.Dataframe(label="Parallel Shock ±100bp (bucketed)")
 
 
 
 
 
 
 
63
 
64
- with gr.Row():
65
- excel_file = gr.File(label="Excel export", interactive=False)
66
- pdf_file = gr.File(label="PDF export", interactive=False)
 
 
 
 
 
 
 
67
 
68
- btn.click(
69
- fn=run_dashboard,
70
- outputs=[
71
- as_of,
72
- k1,
73
- k2,
74
- k3,
75
- chart,
76
- t1m,
77
- t1s,
78
- t1c,
79
- irr,
80
- shocks,
81
- excel_file,
82
- pdf_file,
83
- ],
84
- )
85
 
86
- if __name__ == "__main__":
87
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import os
2
+ from datetime import datetime
3
+ from pathlib import Path
4
+ from typing import Dict, Any, Tuple, Any
 
 
 
 
 
 
 
5
 
6
+ import duckdb
7
+ import pandas as pd
8
+ import numpy as np
9
+ import matplotlib.pyplot as plt
10
+ import gradio as gr
11
+ from pydantic import BaseModel
12
+ from reportlab.lib.pagesizes import A4
13
+ from reportlab.lib.units import mm
14
+ from reportlab.pdfgen import canvas
15
 
16
+ # -------------------------------------------------------------------
17
+ # Basic configuration
18
+ # -------------------------------------------------------------------
19
+ APP_TITLE = "ALCO Liquidity & Interest-Rate Risk Dashboard"
20
+ TABLE_FQN = "my_db.main.masterdataset_v"
21
+ VIEW_FQN = "my_db.main.positions_v"
22
+ EXPORT_DIR = Path("exports")
23
+ EXPORT_DIR.mkdir(exist_ok=True)
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
 
26
+ # -------------------------------------------------------------------
27
+ # MotherDuck connection
28
+ # -------------------------------------------------------------------
29
+ def connect_md() -> duckdb.DuckDBPyConnection:
30
+ token = os.environ.get("MOTHERDUCK_TOKEN", "")
31
+ if not token:
32
+ raise RuntimeError("MOTHERDUCK_TOKEN is not set. Add it as a Space secret.")
33
+ conn = duckdb.connect(f"md:?motherduck_token={token}")
34
+ return conn
35
 
 
 
36
 
37
+ # -------------------------------------------------------------------
38
+ # SQL snippets
39
+ # -------------------------------------------------------------------
40
+ CREATE_VIEW_SQL = f"""
41
+ CREATE OR REPLACE VIEW {VIEW_FQN} AS
42
+ SELECT
43
+ as_of_date,
44
+ product,
45
+ months,
46
+ segments,
47
+ currency,
48
+ Portfolio_value,
49
+ Interest_rate,
50
+ days_to_maturity,
51
+ CASE
52
+ WHEN lower(product) IN ('fd','term_deposit','td','savings','current','call','repo_liab') THEN 'SoF'
53
+ WHEN lower(product) IN ('loan','overdraft','advances','bills','bill','tbond','t-bond','tbill','t-bill','repo_asset') THEN 'Assets'
54
+ ELSE 'Unknown'
55
+ END AS bucket
56
+ FROM {TABLE_FQN};
57
+ """
58
 
59
+ MAX_DATE_SQL = f"""
60
+ WITH maxd AS (SELECT max(as_of_date) AS d FROM {VIEW_FQN})
61
+ SELECT d FROM maxd;
62
+ """
63
 
64
+ KPI_SQL = f"""
65
+ WITH maxd AS (SELECT max(as_of_date) AS d FROM {VIEW_FQN}),
66
+ t1 AS (
67
+ SELECT p.bucket, SUM(p.Portfolio_value) AS amt
68
+ FROM {VIEW_FQN} p
69
+ JOIN maxd m ON p.as_of_date = m.d
70
+ WHERE p.days_to_maturity <= 1
71
+ GROUP BY p.bucket
72
+ )
73
+ SELECT
74
+ COALESCE(SUM(CASE WHEN bucket='Assets' THEN amt END),0) AS assets_t1,
75
+ COALESCE(SUM(CASE WHEN bucket='SoF' THEN amt END),0) AS sof_t1,
76
+ COALESCE(SUM(CASE WHEN bucket='Assets' THEN amt END),0)
77
+ - COALESCE(SUM(CASE WHEN bucket='SoF' THEN amt END),0) AS net_gap_t1
78
+ FROM t1;
79
+ """
80
 
81
+ LADDER_SQL = f"""
82
+ WITH maxd AS (SELECT max(as_of_date) AS d FROM {VIEW_FQN})
83
+ SELECT
84
+ CASE
85
+ WHEN p.days_to_maturity <= 1 THEN 'T+1'
86
+ WHEN p.days_to_maturity BETWEEN 2 AND 7 THEN 'T+2..7'
87
+ WHEN p.days_to_maturity BETWEEN 8 AND 30 THEN 'T+8..30'
88
+ ELSE 'T+31+'
89
+ END AS time_bucket,
90
+ p.bucket,
91
+ SUM(p.Portfolio_value) AS amount
92
+ FROM {VIEW_FQN} p
93
+ JOIN maxd m ON p.as_of_date = m.d
94
+ GROUP BY 1,2
95
+ ORDER BY CASE time_bucket
96
+ WHEN 'T+1' THEN 1 WHEN 'T+2..7' THEN 2 WHEN 'T+8..30' THEN 3 ELSE 4 END,
97
+ p.bucket;
98
+ """
99
 
100
+ T1_BY_MONTH_SQL = f"""
101
+ WITH maxd AS (SELECT max(as_of_date) AS d FROM {VIEW_FQN})
102
+ SELECT p.bucket, p.months, SUM(p.Portfolio_value) AS amount
103
+ FROM {VIEW_FQN} p
104
+ JOIN maxd m ON p.as_of_date = m.d
105
+ WHERE p.days_to_maturity <= 1
106
+ GROUP BY 1,2
107
+ ORDER BY p.bucket, amount DESC
108
+ LIMIT 50;
109
+ """
110
 
111
+ T1_BY_SEGMENT_SQL = f"""
112
+ WITH maxd AS (SELECT max(as_of_date) AS d FROM {VIEW_FQN})
113
+ SELECT p.bucket, p.segments, SUM(p.Portfolio_value) AS amount
114
+ FROM {VIEW_FQN} p
115
+ JOIN maxd m ON p.as_of_date = m.d
116
+ WHERE p.days_to_maturity <= 1
117
+ GROUP BY 1,2
118
+ ORDER BY p.bucket, amount DESC
119
+ LIMIT 50;
120
+ """
121
 
122
+ T1_BY_CCY_SQL = f"""
123
+ WITH maxd AS (SELECT max(as_of_date) AS d FROM {VIEW_FQN})
124
+ SELECT p.bucket, p.currency, SUM(p.Portfolio_value) AS amount
125
+ FROM {VIEW_FQN} p
126
+ JOIN maxd m ON p.as_of_date = m.d
127
+ WHERE p.days_to_maturity <= 1
128
+ GROUP BY 1,2
129
+ ORDER BY p.bucket, amount DESC;
130
+ """
 
 
 
 
 
 
 
 
131
 
132
+ IRR_SQL = f"""
133
+ WITH maxd AS (SELECT max(as_of_date) AS d FROM {VIEW_FQN}),
134
+ base AS (
135
+ SELECT
136
+ p.bucket,
137
+ p.Portfolio_value AS pv,
138
+ (p.Interest_rate / 100.0) AS y,
139
+ CASE
140
+ WHEN p.days_to_maturity IS NOT NULL THEN p.days_to_maturity/365.0
141
+ WHEN p.months IS NOT NULL THEN p.months/12.0
142
+ ELSE NULL
143
+ END AS T_years
144
+ FROM {VIEW_FQN} p
145
+ JOIN maxd m ON p.as_of_date = m.d
146
+ WHERE p.Portfolio_value IS NOT NULL
147
+ ),
148
+ metrics AS (
149
+ SELECT
150
+ bucket,
151
+ pv,
152
+ CASE WHEN T_years IS NULL THEN NULL
153
+ WHEN y IS NULL THEN T_years
154
+ ELSE T_years/(1.0+y) END AS dur_mod,
155
+ CASE WHEN T_years IS NULL THEN NULL
156
+ WHEN y IS NULL THEN T_years*(T_years+1.0)
157
+ ELSE (T_years*(T_years+1.0))/POWER(1.0+y,2) END AS convexity_approx,
158
+ CASE WHEN T_years IS NULL THEN NULL
159
+ ELSE pv * (CASE WHEN y IS NULL THEN T_years ELSE T_years/(1.0+y) END) * 0.0001 END AS dv01
160
+ FROM base
161
+ ),
162
+ agg AS (
163
+ SELECT
164
+ bucket,
165
+ SUM(pv) AS pv_sum,
166
+ SUM(pv * dur_mod) / NULLIF(SUM(pv),0) AS dur_mod_port,
167
+ SUM(dv01) AS dv01_sum
168
+ FROM metrics
169
+ GROUP BY bucket
170
+ )
171
+ SELECT
172
+ COALESCE(MAX(CASE WHEN bucket='Assets' THEN pv_sum END),0) AS assets_pv,
173
+ COALESCE(MAX(CASE WHEN bucket='SoF' THEN pv_sum END),0) AS sof_pv,
174
+ COALESCE(MAX(CASE WHEN bucket='Assets' THEN dur_mod_port END),0) AS assets_dur_mod,
175
+ COALESCE(MAX(CASE WHEN bucket='SoF' THEN dur_mod_port END),0) AS sof_dur_mod,
176
+ COALESCE(MAX(CASE WHEN bucket='Assets' THEN dur_mod_port END),0)
177
+ - COALESCE(MAX(CASE WHEN bucket='SoF' THEN dur_mod_port END),0) AS duration_gap,
178
+ COALESCE(MAX(CASE WHEN bucket='Assets' THEN dv01_sum END),0)
179
+ - COALESCE(MAX(CASE WHEN bucket='SoF' THEN dv01_sum END),0) AS net_dv01
180
+ FROM agg;
181
+ """
182
+
183
+ SHOCK_SQL = f"""
184
+ WITH maxd AS (SELECT max(as_of_date) AS d FROM {VIEW_FQN}),
185
+ base AS (
186
+ SELECT
187
+ p.bucket,
188
+ p.Portfolio_value AS pv,
189
+ (p.Interest_rate / 100.0) AS y,
190
+ CASE
191
+ WHEN p.days_to_maturity IS NOT NULL THEN p.days_to_maturity/365.0
192
+ WHEN p.months IS NOT NULL THEN p.months/12.0
193
+ END AS T_years
194
+ FROM {VIEW_FQN} p
195
+ JOIN maxd m ON p.as_of_date = m.d
196
+ ),
197
+ k AS (
198
+ SELECT
199
+ bucket, pv,
200
+ CASE WHEN T_years IS NULL THEN NULL
201
+ WHEN y IS NULL THEN T_years
202
+ ELSE T_years/(1.0+y) END AS dur_mod,
203
+ CASE WHEN T_years IS NULL THEN NULL
204
+ WHEN y IS NULL THEN T_years*(T_years+1.0)
205
+ ELSE (T_years*(T_years+1.0))/POWER(1.0+y,2) END AS convexity_approx
206
+ FROM base
207
+ ),
208
+ shock AS (
209
+ SELECT
210
+ bucket,
211
+ SUM((- pv * dur_mod * 0.01) + (0.5 * pv * convexity_approx * POWER(0.01,2))) AS dPV_up_100bp,
212
+ SUM((+ pv * dur_mod * 0.01) + (0.5 * pv * convexity_approx * POWER(-0.01,2))) AS dPV_dn_100bp
213
+ F