WR Development Fund
UI improvement: Move charts side-by-side with summary
f635e9e
import gradio as gr
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
import plotly.graph_objects as go
from plotly.subplots import make_subplots
def calculate_fund_model(
fund_size,
lp_annual_return,
project_cost,
project_exit,
project_timeline_months,
dev_fee_pct,
construction_markup_pct,
fund_term_years,
stagger_months,
distribution_start_month
):
"""
Complete fund model with all cash flows
"""
# Initialize
months = fund_term_years * 12
cash_flows = []
projects = []
# Month 0: LP invests
cash_flows.append({
'month': 0,
'type': 'LP Investment',
'amount': fund_size,
'cash_balance': fund_size,
'lp_distributions': 0,
'gp_fees': 0,
'projects_active': 0
})
current_cash = fund_size
gp_total_fees = 0
lp_total_distributions = 0
project_id = 0
# Calculate project economics
hard_cost = project_cost * 0.5 # Assume 50% is construction
dev_fee = project_cost * (dev_fee_pct / 100)
construction_profit = hard_cost * (construction_markup_pct / 100)
gp_fee_per_project = dev_fee + construction_profit
net_exit = project_exit # Already net of selling costs
profit_per_project = net_exit - project_cost
# Deploy projects on staggered schedule
for month in range(1, months + 1):
month_events = []
# FIRST: Process project exits (get cash from completed projects)
exiting_projects = [p for p in projects if p['exit_month'] == month]
if exiting_projects:
for proj in exiting_projects:
current_cash += proj['exit']
gp_total_fees += proj['gp_fees']
current_cash -= proj['gp_fees'] # GP takes fees from exits
month_events.append(f"Project {proj['id']} exited: ${proj['exit']/1e6:.1f}M")
# SECOND: Pay LP distributions (fulfill obligations)
lp_distribution = 0
if month >= distribution_start_month and month % 3 == 0:
quarterly_distribution = fund_size * (lp_annual_return / 100) / 4
if current_cash >= quarterly_distribution:
lp_distribution = quarterly_distribution
current_cash -= lp_distribution
lp_total_distributions += lp_distribution
month_events.append(f"LP distribution: ${lp_distribution/1e6:.2f}M")
# THIRD: Deploy new projects (use remaining cash)
# Don't start projects that won't complete before fund termination
# Deploy 1 project at a time on staggered schedule (starting month 1)
if (month == 1 or (month - 1) % stagger_months == 0) and month + project_timeline_months <= months:
# Deploy 1 project if cash available
if current_cash >= project_cost:
project_id += 1
projects.append({
'id': project_id,
'start_month': month,
'exit_month': month + project_timeline_months,
'deploy': project_cost,
'exit': net_exit,
'gp_fees': gp_fee_per_project
})
current_cash -= project_cost
month_events.append(f"Deployed project {project_id} (${project_cost/1e6:.1f}M)")
# Count active projects
active_projects = len([p for p in projects if p['start_month'] <= month < p['exit_month']])
# Record month
cash_flows.append({
'month': month,
'type': ', '.join(month_events) if month_events else 'No activity',
'amount': 0,
'cash_balance': current_cash,
'lp_distributions': lp_total_distributions,
'gp_fees': gp_total_fees,
'projects_active': active_projects
})
# Final settlement at fund termination
total_lp_accrued = fund_size * (lp_annual_return / 100) * fund_term_years
lp_owed = fund_size + total_lp_accrued - lp_total_distributions
if current_cash >= lp_owed:
final_lp_payment = lp_owed
gp_residual = current_cash - lp_owed
else:
final_lp_payment = current_cash
gp_residual = 0
lp_total_received = lp_total_distributions + final_lp_payment
lp_total_return_pct = ((lp_total_received - fund_size) / fund_size) * 100
lp_irr = (lp_total_received / fund_size) ** (1 / fund_term_years) - 1
gp_total_revenue = gp_total_fees + gp_residual
# Create results summary
results = {
'fund_size': fund_size,
'fund_term_years': fund_term_years,
'total_projects': len(projects),
'lp_total_distributions': lp_total_distributions,
'lp_final_payment': final_lp_payment,
'lp_total_received': lp_total_received,
'lp_total_return_pct': lp_total_return_pct,
'lp_irr': lp_irr * 100,
'gp_fees': gp_total_fees,
'gp_residual': gp_residual,
'gp_total_revenue': gp_total_revenue,
'gp_pct_of_fund': (gp_total_revenue / fund_size) * 100
}
# Create DataFrame for cash flows
df_cashflows = pd.DataFrame(cash_flows)
return results, df_cashflows, projects
def create_visualizations(results, df_cashflows, projects):
"""
Create Plotly visualizations
"""
# Create subplots
fig = make_subplots(
rows=2, cols=2,
subplot_titles=(
'Cash Balance Over Time',
'Active Projects Timeline',
'Cumulative Distributions',
'LP vs GP Economics'
),
specs=[
[{"type": "scatter"}, {"type": "scatter"}],
[{"type": "scatter"}, {"type": "bar"}]
]
)
# Chart 1: Cash balance
fig.add_trace(
go.Scatter(
x=df_cashflows['month'],
y=df_cashflows['cash_balance'] / 1e6,
mode='lines',
name='Cash Balance',
line=dict(color='blue', width=2)
),
row=1, col=1
)
# Chart 2: Active projects
fig.add_trace(
go.Scatter(
x=df_cashflows['month'],
y=df_cashflows['projects_active'],
mode='lines',
name='Active Projects',
line=dict(color='green', width=2),
fill='tozeroy'
),
row=1, col=2
)
# Chart 3: Cumulative distributions
fig.add_trace(
go.Scatter(
x=df_cashflows['month'],
y=df_cashflows['lp_distributions'] / 1e6,
mode='lines',
name='LP Distributions',
line=dict(color='purple', width=2)
),
row=2, col=1
)
fig.add_trace(
go.Scatter(
x=df_cashflows['month'],
y=df_cashflows['gp_fees'] / 1e6,
mode='lines',
name='GP Fees',
line=dict(color='orange', width=2)
),
row=2, col=1
)
# Chart 4: LP vs GP bar chart
fig.add_trace(
go.Bar(
x=['LP Total', 'GP Total'],
y=[results['lp_total_received'] / 1e6, results['gp_total_revenue'] / 1e6],
marker_color=['blue', 'orange'],
text=[f"${results['lp_total_received']/1e6:.1f}M", f"${results['gp_total_revenue']/1e6:.1f}M"],
textposition='auto'
),
row=2, col=2
)
# Update layout
fig.update_xaxes(title_text="Month", row=1, col=1)
fig.update_xaxes(title_text="Month", row=1, col=2)
fig.update_xaxes(title_text="Month", row=2, col=1)
fig.update_yaxes(title_text="Cash ($M)", row=1, col=1)
fig.update_yaxes(title_text="# Projects", row=1, col=2)
fig.update_yaxes(title_text="Cumulative ($M)", row=2, col=1)
fig.update_yaxes(title_text="Total Received ($M)", row=2, col=2)
fig.update_layout(height=800, showlegend=True, title_text="Fund Performance Dashboard")
return fig
def format_results(results):
"""
Format results into readable markdown
"""
md = f"""
# Fund Performance Summary
## Fund Overview
- **Fund Size**: ${results['fund_size']/1e6:.1f}M
- **Term**: {results['fund_term_years']} years
- **Total Projects**: {results['total_projects']}
## LP Economics
- **Total Distributions During Hold**: ${results['lp_total_distributions']/1e6:.2f}M
- **Final Payment at Termination**: ${results['lp_final_payment']/1e6:.2f}M
- **Total LP Received**: ${results['lp_total_received']/1e6:.2f}M
- **Total Return**: {results['lp_total_return_pct']:.1f}%
- **IRR**: {results['lp_irr']:.1f}%
- **Simple Annual**: {results['lp_total_return_pct'] / results['fund_term_years']:.1f}%
## GP Economics
- **Fees (Dev + Construction)**: ${results['gp_fees']/1e6:.2f}M
- **Residual Profit**: ${results['gp_residual']/1e6:.2f}M
- **Total GP Revenue**: ${results['gp_total_revenue']/1e6:.2f}M
- **GP % of Fund Size**: {results['gp_pct_of_fund']:.1f}%
## Investment Comparison
- **LP receives**: ${results['lp_total_received']/1e6:.1f}M ({results['lp_total_return_pct']:.0f}% return)
- **GP receives**: ${results['gp_total_revenue']/1e6:.1f}M on $0 invested
- **Ratio**: LP gets {results['lp_total_received'] / results['gp_total_revenue']:.1f}x what GP gets
"""
return md
def run_model(fund_size_m, lp_return, proj_cost_m, proj_exit_m, proj_months,
dev_fee, constr_markup, term_years, stagger, dist_start):
"""
Main function to run model and return all outputs
"""
fund_size = fund_size_m * 1e6
proj_cost = proj_cost_m * 1e6
proj_exit = proj_exit_m * 1e6
results, df_cashflows, projects = calculate_fund_model(
fund_size, lp_return, proj_cost, proj_exit, proj_months,
dev_fee, constr_markup, term_years, stagger, dist_start
)
summary_md = format_results(results)
fig = create_visualizations(results, df_cashflows, projects)
# Create cashflow table (show last 24 months)
df_display = df_cashflows.tail(24).copy()
df_display['cash_balance'] = df_display['cash_balance'].apply(lambda x: f"${x/1e6:.2f}M")
df_display['lp_distributions'] = df_display['lp_distributions'].apply(lambda x: f"${x/1e6:.2f}M")
df_display['gp_fees'] = df_display['gp_fees'].apply(lambda x: f"${x/1e6:.2f}M")
# Projects table
df_projects = pd.DataFrame(projects)
if len(df_projects) > 0:
df_projects['deploy'] = df_projects['deploy'].apply(lambda x: f"${x/1e6:.1f}M")
df_projects['exit'] = df_projects['exit'].apply(lambda x: f"${x/1e6:.1f}M")
df_projects['gp_fees'] = df_projects['gp_fees'].apply(lambda x: f"${x/1e3:.0f}K")
return summary_md, fig, df_display, df_projects
# Create Gradio interface
with gr.Blocks(title="WR Development Fund I - Financial Model", theme=gr.themes.Soft(), css="""
.gradio-container {max-width: 100% !important; padding: 20px !important;}
""") as demo:
gr.Markdown("""
# 🏗️ WR Development Fund I - Financial Modeling Tool
### Brooklyn Condo Conversion Strategy
Model complete fund economics with staggered project deployment, cash flows, and LP/GP returns.
""")
# Three-column layout for parameters
with gr.Row(equal_height=True):
# Column 1: Fund Parameters
with gr.Column(scale=1):
gr.Markdown("## Fund Parameters")
fund_size = gr.Slider(
minimum=10, maximum=50, value=30, step=1,
label="Fund Size ($M)"
)
lp_return = gr.Slider(
minimum=12, maximum=30, value=15, step=1,
label="LP Annual Return (%)"
)
term_years = gr.Slider(
minimum=3, maximum=10, value=7, step=1,
label="Fund Term (Years)"
)
dist_start = gr.Slider(
minimum=0, maximum=48, value=24, step=6,
label="Distribution Start Month"
)
# Column 2: Project Economics
with gr.Column(scale=1):
gr.Markdown("## Project Economics")
proj_cost = gr.Slider(
minimum=2, maximum=8, value=4.2, step=0.1,
label="Project Cost ($M)"
)
proj_exit = gr.Slider(
minimum=4, maximum=15, value=7.5, step=0.1,
label="Project Exit ($M)"
)
proj_timeline = gr.Slider(
minimum=12, maximum=36, value=24, step=3,
label="Project Timeline (Months)"
)
stagger = gr.Slider(
minimum=1, maximum=12, value=3, step=1,
label="Deploy Every N Months"
)
# Column 3: GP Fee Structure (shorter, with button at bottom)
with gr.Column(scale=1):
gr.Markdown("## GP Fee Structure")
dev_fee = gr.Slider(
minimum=2, maximum=8, value=5, step=0.5,
label="Development Fee (%)"
)
constr_markup = gr.Slider(
minimum=8, maximum=30, value=20, step=1,
label="Construction Markup (%)"
)
gr.Markdown("<br>" * 2) # Spacer to align button
calculate_btn = gr.Button("🚀 Calculate Fund Model", variant="primary", size="lg", scale=1)
# Two-column layout: Summary on left, Charts on right
with gr.Row():
with gr.Column(scale=1):
summary_output = gr.Markdown()
with gr.Column(scale=2):
chart_output = gr.Plot()
# Full-width cash flow table
with gr.Row():
with gr.Column():
gr.Markdown("### Recent Cash Flows (Last 24 Months)")
cashflow_table = gr.Dataframe()
# Full-width projects table
with gr.Row():
with gr.Column():
gr.Markdown("### All Projects")
projects_table = gr.Dataframe()
calculate_btn.click(
fn=run_model,
inputs=[fund_size, lp_return, proj_cost, proj_exit, proj_timeline,
dev_fee, constr_markup, term_years, stagger, dist_start],
outputs=[summary_output, chart_output, cashflow_table, projects_table]
)
gr.Markdown("""
---
**Optimized for:** $30M fund, 7-year term, 15% LP return, $7.5M exits, 3-month deployment cycle
""")
if __name__ == "__main__":
# Launch with appropriate settings for local vs cloud deployment
import os
is_hf_space = os.getenv("SPACE_ID") is not None
if is_hf_space:
# Hugging Face Spaces deployment
demo.launch(
server_name="0.0.0.0",
server_port=7860,
show_error=True
)
else:
# Local deployment
demo.launch(
share=True,
server_name="0.0.0.0",
show_error=True
)