WR Development Fund Claude Sonnet 4.5 commited on
Commit
f635e9e
ยท
1 Parent(s): cce3db1

UI improvement: Move charts side-by-side with summary

Browse files

- Charts now appear to the right of economics summary
- Better use of horizontal space
- Summary (scale=1) on left, Charts (scale=2) on right
- Reduces vertical scrolling

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

Files changed (2) hide show
  1. app.py +3 -5
  2. fundmodel.py +439 -0
app.py CHANGED
@@ -386,14 +386,12 @@ with gr.Blocks(title="WR Development Fund I - Financial Model", theme=gr.themes.
386
 
387
  calculate_btn = gr.Button("๐Ÿš€ Calculate Fund Model", variant="primary", size="lg", scale=1)
388
 
389
- # Full-width summary results
390
  with gr.Row():
391
- with gr.Column():
392
  summary_output = gr.Markdown()
393
 
394
- # Full-width charts
395
- with gr.Row():
396
- with gr.Column():
397
  chart_output = gr.Plot()
398
 
399
  # Full-width cash flow table
 
386
 
387
  calculate_btn = gr.Button("๐Ÿš€ Calculate Fund Model", variant="primary", size="lg", scale=1)
388
 
389
+ # Two-column layout: Summary on left, Charts on right
390
  with gr.Row():
391
+ with gr.Column(scale=1):
392
  summary_output = gr.Markdown()
393
 
394
+ with gr.Column(scale=2):
 
 
395
  chart_output = gr.Plot()
396
 
397
  # Full-width cash flow table
fundmodel.py ADDED
@@ -0,0 +1,439 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import pandas as pd
3
+ import numpy as np
4
+ from datetime import datetime, timedelta
5
+ import plotly.graph_objects as go
6
+ from plotly.subplots import make_subplots
7
+
8
+ def calculate_fund_model(
9
+ fund_size,
10
+ lp_annual_return,
11
+ project_cost,
12
+ project_exit,
13
+ project_timeline_months,
14
+ dev_fee_pct,
15
+ construction_markup_pct,
16
+ fund_term_years,
17
+ stagger_months,
18
+ distribution_start_month
19
+ ):
20
+ """
21
+ Complete fund model with all cash flows
22
+ """
23
+
24
+ # Initialize
25
+ months = fund_term_years * 12
26
+ cash_flows = []
27
+ projects = []
28
+
29
+ # Month 0: LP invests
30
+ cash_flows.append({
31
+ 'month': 0,
32
+ 'type': 'LP Investment',
33
+ 'amount': fund_size,
34
+ 'cash_balance': fund_size,
35
+ 'lp_distributions': 0,
36
+ 'gp_fees': 0,
37
+ 'projects_active': 0
38
+ })
39
+
40
+ current_cash = fund_size
41
+ gp_total_fees = 0
42
+ lp_total_distributions = 0
43
+ project_id = 0
44
+
45
+ # Calculate project economics
46
+ hard_cost = project_cost * 0.5 # Assume 50% is construction
47
+ dev_fee = project_cost * (dev_fee_pct / 100)
48
+ construction_profit = hard_cost * (construction_markup_pct / 100)
49
+ gp_fee_per_project = dev_fee + construction_profit
50
+ net_exit = project_exit # Already net of selling costs
51
+ profit_per_project = net_exit - project_cost
52
+
53
+ # Deploy projects on staggered schedule
54
+ for month in range(1, months + 1):
55
+ month_events = []
56
+
57
+ # FIRST: Process project exits (get cash from completed projects)
58
+ exiting_projects = [p for p in projects if p['exit_month'] == month]
59
+ if exiting_projects:
60
+ for proj in exiting_projects:
61
+ current_cash += proj['exit']
62
+ gp_total_fees += proj['gp_fees']
63
+ current_cash -= proj['gp_fees'] # GP takes fees from exits
64
+ month_events.append(f"Project {proj['id']} exited: ${proj['exit']/1e6:.1f}M")
65
+
66
+ # SECOND: Pay LP distributions (fulfill obligations)
67
+ lp_distribution = 0
68
+ if month >= distribution_start_month and month % 3 == 0:
69
+ quarterly_distribution = fund_size * (lp_annual_return / 100) / 4
70
+ if current_cash >= quarterly_distribution:
71
+ lp_distribution = quarterly_distribution
72
+ current_cash -= lp_distribution
73
+ lp_total_distributions += lp_distribution
74
+ month_events.append(f"LP distribution: ${lp_distribution/1e6:.2f}M")
75
+
76
+ # THIRD: Deploy new projects (use remaining cash)
77
+ # Don't start projects that won't complete before fund termination
78
+ # Deploy 1 project at a time on staggered schedule (starting month 1)
79
+ if (month == 1 or (month - 1) % stagger_months == 0) and month + project_timeline_months <= months:
80
+ # Deploy 1 project if cash available
81
+ if current_cash >= project_cost:
82
+ project_id += 1
83
+ projects.append({
84
+ 'id': project_id,
85
+ 'start_month': month,
86
+ 'exit_month': month + project_timeline_months,
87
+ 'deploy': project_cost,
88
+ 'exit': net_exit,
89
+ 'gp_fees': gp_fee_per_project
90
+ })
91
+
92
+ current_cash -= project_cost
93
+ month_events.append(f"Deployed project {project_id} (${project_cost/1e6:.1f}M)")
94
+
95
+ # Count active projects
96
+ active_projects = len([p for p in projects if p['start_month'] <= month < p['exit_month']])
97
+
98
+ # Record month
99
+ cash_flows.append({
100
+ 'month': month,
101
+ 'type': ', '.join(month_events) if month_events else 'No activity',
102
+ 'amount': 0,
103
+ 'cash_balance': current_cash,
104
+ 'lp_distributions': lp_total_distributions,
105
+ 'gp_fees': gp_total_fees,
106
+ 'projects_active': active_projects
107
+ })
108
+
109
+ # Final settlement at fund termination
110
+ total_lp_accrued = fund_size * (lp_annual_return / 100) * fund_term_years
111
+ lp_owed = fund_size + total_lp_accrued - lp_total_distributions
112
+
113
+ if current_cash >= lp_owed:
114
+ final_lp_payment = lp_owed
115
+ gp_residual = current_cash - lp_owed
116
+ else:
117
+ final_lp_payment = current_cash
118
+ gp_residual = 0
119
+
120
+ lp_total_received = lp_total_distributions + final_lp_payment
121
+ lp_total_return_pct = ((lp_total_received - fund_size) / fund_size) * 100
122
+ lp_irr = (lp_total_received / fund_size) ** (1 / fund_term_years) - 1
123
+
124
+ gp_total_revenue = gp_total_fees + gp_residual
125
+
126
+ # Create results summary
127
+ results = {
128
+ 'fund_size': fund_size,
129
+ 'fund_term_years': fund_term_years,
130
+ 'total_projects': len(projects),
131
+ 'lp_total_distributions': lp_total_distributions,
132
+ 'lp_final_payment': final_lp_payment,
133
+ 'lp_total_received': lp_total_received,
134
+ 'lp_total_return_pct': lp_total_return_pct,
135
+ 'lp_irr': lp_irr * 100,
136
+ 'gp_fees': gp_total_fees,
137
+ 'gp_residual': gp_residual,
138
+ 'gp_total_revenue': gp_total_revenue,
139
+ 'gp_pct_of_fund': (gp_total_revenue / fund_size) * 100
140
+ }
141
+
142
+ # Create DataFrame for cash flows
143
+ df_cashflows = pd.DataFrame(cash_flows)
144
+
145
+ return results, df_cashflows, projects
146
+
147
+ def create_visualizations(results, df_cashflows, projects):
148
+ """
149
+ Create Plotly visualizations
150
+ """
151
+
152
+ # Create subplots
153
+ fig = make_subplots(
154
+ rows=2, cols=2,
155
+ subplot_titles=(
156
+ 'Cash Balance Over Time',
157
+ 'Active Projects Timeline',
158
+ 'Cumulative Distributions',
159
+ 'LP vs GP Economics'
160
+ ),
161
+ specs=[
162
+ [{"type": "scatter"}, {"type": "scatter"}],
163
+ [{"type": "scatter"}, {"type": "bar"}]
164
+ ]
165
+ )
166
+
167
+ # Chart 1: Cash balance
168
+ fig.add_trace(
169
+ go.Scatter(
170
+ x=df_cashflows['month'],
171
+ y=df_cashflows['cash_balance'] / 1e6,
172
+ mode='lines',
173
+ name='Cash Balance',
174
+ line=dict(color='blue', width=2)
175
+ ),
176
+ row=1, col=1
177
+ )
178
+
179
+ # Chart 2: Active projects
180
+ fig.add_trace(
181
+ go.Scatter(
182
+ x=df_cashflows['month'],
183
+ y=df_cashflows['projects_active'],
184
+ mode='lines',
185
+ name='Active Projects',
186
+ line=dict(color='green', width=2),
187
+ fill='tozeroy'
188
+ ),
189
+ row=1, col=2
190
+ )
191
+
192
+ # Chart 3: Cumulative distributions
193
+ fig.add_trace(
194
+ go.Scatter(
195
+ x=df_cashflows['month'],
196
+ y=df_cashflows['lp_distributions'] / 1e6,
197
+ mode='lines',
198
+ name='LP Distributions',
199
+ line=dict(color='purple', width=2)
200
+ ),
201
+ row=2, col=1
202
+ )
203
+
204
+ fig.add_trace(
205
+ go.Scatter(
206
+ x=df_cashflows['month'],
207
+ y=df_cashflows['gp_fees'] / 1e6,
208
+ mode='lines',
209
+ name='GP Fees',
210
+ line=dict(color='orange', width=2)
211
+ ),
212
+ row=2, col=1
213
+ )
214
+
215
+ # Chart 4: LP vs GP bar chart
216
+ fig.add_trace(
217
+ go.Bar(
218
+ x=['LP Total', 'GP Total'],
219
+ y=[results['lp_total_received'] / 1e6, results['gp_total_revenue'] / 1e6],
220
+ marker_color=['blue', 'orange'],
221
+ text=[f"${results['lp_total_received']/1e6:.1f}M", f"${results['gp_total_revenue']/1e6:.1f}M"],
222
+ textposition='auto'
223
+ ),
224
+ row=2, col=2
225
+ )
226
+
227
+ # Update layout
228
+ fig.update_xaxes(title_text="Month", row=1, col=1)
229
+ fig.update_xaxes(title_text="Month", row=1, col=2)
230
+ fig.update_xaxes(title_text="Month", row=2, col=1)
231
+
232
+ fig.update_yaxes(title_text="Cash ($M)", row=1, col=1)
233
+ fig.update_yaxes(title_text="# Projects", row=1, col=2)
234
+ fig.update_yaxes(title_text="Cumulative ($M)", row=2, col=1)
235
+ fig.update_yaxes(title_text="Total Received ($M)", row=2, col=2)
236
+
237
+ fig.update_layout(height=800, showlegend=True, title_text="Fund Performance Dashboard")
238
+
239
+ return fig
240
+
241
+ def format_results(results):
242
+ """
243
+ Format results into readable markdown
244
+ """
245
+
246
+ md = f"""
247
+ # Fund Performance Summary
248
+
249
+ ## Fund Overview
250
+ - **Fund Size**: ${results['fund_size']/1e6:.1f}M
251
+ - **Term**: {results['fund_term_years']} years
252
+ - **Total Projects**: {results['total_projects']}
253
+
254
+ ## LP Economics
255
+ - **Total Distributions During Hold**: ${results['lp_total_distributions']/1e6:.2f}M
256
+ - **Final Payment at Termination**: ${results['lp_final_payment']/1e6:.2f}M
257
+ - **Total LP Received**: ${results['lp_total_received']/1e6:.2f}M
258
+ - **Total Return**: {results['lp_total_return_pct']:.1f}%
259
+ - **IRR**: {results['lp_irr']:.1f}%
260
+ - **Simple Annual**: {results['lp_total_return_pct'] / results['fund_term_years']:.1f}%
261
+
262
+ ## GP Economics
263
+ - **Fees (Dev + Construction)**: ${results['gp_fees']/1e6:.2f}M
264
+ - **Residual Profit**: ${results['gp_residual']/1e6:.2f}M
265
+ - **Total GP Revenue**: ${results['gp_total_revenue']/1e6:.2f}M
266
+ - **GP % of Fund Size**: {results['gp_pct_of_fund']:.1f}%
267
+
268
+ ## Investment Comparison
269
+ - **LP receives**: ${results['lp_total_received']/1e6:.1f}M ({results['lp_total_return_pct']:.0f}% return)
270
+ - **GP receives**: ${results['gp_total_revenue']/1e6:.1f}M on $0 invested
271
+ - **Ratio**: LP gets {results['lp_total_received'] / results['gp_total_revenue']:.1f}x what GP gets
272
+ """
273
+
274
+ return md
275
+
276
+ def run_model(fund_size_m, lp_return, proj_cost_m, proj_exit_m, proj_months,
277
+ dev_fee, constr_markup, term_years, stagger, dist_start):
278
+ """
279
+ Main function to run model and return all outputs
280
+ """
281
+
282
+ fund_size = fund_size_m * 1e6
283
+ proj_cost = proj_cost_m * 1e6
284
+ proj_exit = proj_exit_m * 1e6
285
+
286
+ results, df_cashflows, projects = calculate_fund_model(
287
+ fund_size, lp_return, proj_cost, proj_exit, proj_months,
288
+ dev_fee, constr_markup, term_years, stagger, dist_start
289
+ )
290
+
291
+ summary_md = format_results(results)
292
+ fig = create_visualizations(results, df_cashflows, projects)
293
+
294
+ # Create cashflow table (show last 24 months)
295
+ df_display = df_cashflows.tail(24).copy()
296
+ df_display['cash_balance'] = df_display['cash_balance'].apply(lambda x: f"${x/1e6:.2f}M")
297
+ df_display['lp_distributions'] = df_display['lp_distributions'].apply(lambda x: f"${x/1e6:.2f}M")
298
+ df_display['gp_fees'] = df_display['gp_fees'].apply(lambda x: f"${x/1e6:.2f}M")
299
+
300
+ # Projects table
301
+ df_projects = pd.DataFrame(projects)
302
+ if len(df_projects) > 0:
303
+ df_projects['deploy'] = df_projects['deploy'].apply(lambda x: f"${x/1e6:.1f}M")
304
+ df_projects['exit'] = df_projects['exit'].apply(lambda x: f"${x/1e6:.1f}M")
305
+ df_projects['gp_fees'] = df_projects['gp_fees'].apply(lambda x: f"${x/1e3:.0f}K")
306
+
307
+ return summary_md, fig, df_display, df_projects
308
+
309
+ # Create Gradio interface
310
+ with gr.Blocks(title="WR Development Fund I - Financial Model", theme=gr.themes.Soft(), css="""
311
+ .gradio-container {max-width: 100% !important; padding: 20px !important;}
312
+ """) as demo:
313
+
314
+ gr.Markdown("""
315
+ # ๐Ÿ—๏ธ WR Development Fund I - Financial Modeling Tool
316
+ ### Brooklyn Condo Conversion Strategy
317
+
318
+ Model complete fund economics with staggered project deployment, cash flows, and LP/GP returns.
319
+ """)
320
+
321
+ # Three-column layout for parameters
322
+ with gr.Row(equal_height=True):
323
+ # Column 1: Fund Parameters
324
+ with gr.Column(scale=1):
325
+ gr.Markdown("## Fund Parameters")
326
+
327
+ fund_size = gr.Slider(
328
+ minimum=10, maximum=50, value=30, step=1,
329
+ label="Fund Size ($M)"
330
+ )
331
+
332
+ lp_return = gr.Slider(
333
+ minimum=12, maximum=30, value=15, step=1,
334
+ label="LP Annual Return (%)"
335
+ )
336
+
337
+ term_years = gr.Slider(
338
+ minimum=3, maximum=10, value=7, step=1,
339
+ label="Fund Term (Years)"
340
+ )
341
+
342
+ dist_start = gr.Slider(
343
+ minimum=0, maximum=48, value=24, step=6,
344
+ label="Distribution Start Month"
345
+ )
346
+
347
+ # Column 2: Project Economics
348
+ with gr.Column(scale=1):
349
+ gr.Markdown("## Project Economics")
350
+
351
+ proj_cost = gr.Slider(
352
+ minimum=2, maximum=8, value=4.2, step=0.1,
353
+ label="Project Cost ($M)"
354
+ )
355
+
356
+ proj_exit = gr.Slider(
357
+ minimum=4, maximum=15, value=7.5, step=0.1,
358
+ label="Project Exit ($M)"
359
+ )
360
+
361
+ proj_timeline = gr.Slider(
362
+ minimum=12, maximum=36, value=24, step=3,
363
+ label="Project Timeline (Months)"
364
+ )
365
+
366
+ stagger = gr.Slider(
367
+ minimum=1, maximum=12, value=3, step=1,
368
+ label="Deploy Every N Months"
369
+ )
370
+
371
+ # Column 3: GP Fee Structure (shorter, with button at bottom)
372
+ with gr.Column(scale=1):
373
+ gr.Markdown("## GP Fee Structure")
374
+
375
+ dev_fee = gr.Slider(
376
+ minimum=2, maximum=8, value=5, step=0.5,
377
+ label="Development Fee (%)"
378
+ )
379
+
380
+ constr_markup = gr.Slider(
381
+ minimum=8, maximum=30, value=20, step=1,
382
+ label="Construction Markup (%)"
383
+ )
384
+
385
+ gr.Markdown("<br>" * 2) # Spacer to align button
386
+
387
+ calculate_btn = gr.Button("๐Ÿš€ Calculate Fund Model", variant="primary", size="lg", scale=1)
388
+
389
+ # Two-column layout: Summary on left, Charts on right
390
+ with gr.Row():
391
+ with gr.Column(scale=1):
392
+ summary_output = gr.Markdown()
393
+
394
+ with gr.Column(scale=2):
395
+ chart_output = gr.Plot()
396
+
397
+ # Full-width cash flow table
398
+ with gr.Row():
399
+ with gr.Column():
400
+ gr.Markdown("### Recent Cash Flows (Last 24 Months)")
401
+ cashflow_table = gr.Dataframe()
402
+
403
+ # Full-width projects table
404
+ with gr.Row():
405
+ with gr.Column():
406
+ gr.Markdown("### All Projects")
407
+ projects_table = gr.Dataframe()
408
+
409
+ calculate_btn.click(
410
+ fn=run_model,
411
+ inputs=[fund_size, lp_return, proj_cost, proj_exit, proj_timeline,
412
+ dev_fee, constr_markup, term_years, stagger, dist_start],
413
+ outputs=[summary_output, chart_output, cashflow_table, projects_table]
414
+ )
415
+
416
+ gr.Markdown("""
417
+ ---
418
+ **Optimized for:** $30M fund, 7-year term, 15% LP return, $7.5M exits, 3-month deployment cycle
419
+ """)
420
+
421
+ if __name__ == "__main__":
422
+ # Launch with appropriate settings for local vs cloud deployment
423
+ import os
424
+ is_hf_space = os.getenv("SPACE_ID") is not None
425
+
426
+ if is_hf_space:
427
+ # Hugging Face Spaces deployment
428
+ demo.launch(
429
+ server_name="0.0.0.0",
430
+ server_port=7860,
431
+ show_error=True
432
+ )
433
+ else:
434
+ # Local deployment
435
+ demo.launch(
436
+ share=True,
437
+ server_name="0.0.0.0",
438
+ show_error=True
439
+ )