Spaces:
Sleeping
Sleeping
| 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 | |
| ) |