Yes, I give a fig... thoughts on markets from Michael Green

Code for 3 Part Series -- The Summer Slide Economy

Executable Code for series

Michael W. Green's avatar
Michael W. Green
Feb 15, 2026
∙ Paid

This is the Python executable code for all simulations in the series starting today. The actual note follows in a few minutes, but this allows me to create a reference link.

Appendix: Python Code

Can be executed in Google AiStudio: https://aistudio.google.com/prompts/new_chat?model=gemini-3-pro-preview. Make sure you have “Code Execution” selected on the lower right portion of your screen.

Phase 1: The “Talent” Extinction Event

import numpy as np
import matplotlib.pyplot as plt

def run_phase_1_final_three_panels_v2():
    # Parameters
    n_agents = 5000 
    T = 40
    dt = 0.1
    steps = int(T / dt)
    
    W0 = 1000.0
    W_barrier = 200.0  # Ruin Barrier
    
    # Groups: (Name, Mu, Sigma, Color, Style)
    # Using distinct colors/styles to ensure all 4 are visible
    groups = [
        ("Normie / High SES", 0.05, 0.10, "tab:blue", "-"),      # Safe / Safe
        ("Normie / Low SES", 0.05, 0.20, "tab:cyan", "--"),      # Safe / Risky
        ("Gifted / High SES", 0.08, 0.20, "tab:green", "-"),     # Risky / Safe
        ("Gifted / Low SES", 0.08, 0.40, "tab:red", "--")        # Risky / Risky
    ]
    
    # Store data
    results_avg = {}
    results_median = {}
    results_survival = {}
    
    np.random.seed(42)
    
    for name, mu, sigma, color, style in groups:
        wealth = np.full(n_agents, W0)
        alive = np.ones(n_agents, dtype=bool)
        
        avg_hist = [W0]
        median_hist = [W0]
        survival_hist = [1.0]
        
        for t in range(steps):
            if np.any(alive):
                z = np.random.normal(0, 1, n_agents)
                growth = (mu - 0.5 * sigma**2) * dt + sigma * np.sqrt(dt) * z
                
                # Update wealth for alive agents
                wealth[alive] *= np.exp(growth[alive])
                
                # Check Ruin
                ruined = (wealth < W_barrier) & alive
                alive[ruined] = False
                wealth[ruined] = 0.0
            
            avg_hist.append(np.mean(wealth))
            median_hist.append(np.median(wealth))
            survival_hist.append(np.mean(alive))
            
        results_avg[name] = avg_hist
        results_median[name] = median_hist
        results_survival[name] = survival_hist

    # Plotting
    fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(18, 6))
    time = np.linspace(0, T, steps + 1)
    
    # Common function to plot with distinct styles
    def plot_lines(ax, data_dict):
        for name, data in data_dict.items():
            # Retrieve style info
            grp = next(g for g in groups if g[0] == name)
            color = grp[3]
            style = grp[4]
            # Plot
            ax.plot(time, data, label=name, color=color, linestyle=style, linewidth=2.5, alpha=0.9)

    # Panel 1: Average
    plot_lines(ax1, results_avg)
    ax1.set_title('Average Wealth (The Illusion)', fontweight='bold')
    ax1.set_ylabel('Wealth ($)')
    ax1.grid(True, alpha=0.3)
    ax1.legend(loc='upper left', fontsize='small')
    
    # Panel 2: Median
    plot_lines(ax2, results_median)
    ax2.set_title('Median Wealth (The Reality)', fontweight='bold')
    ax2.grid(True, alpha=0.3)
    ax2.legend(loc='upper left', fontsize='small')
    
    # Panel 3: Survival
    # Multiply by 100 for percentage
    results_survival_pct = {k: np.array(v)*100 for k, v in results_survival.items()}
    plot_lines(ax3, results_survival_pct)
    ax3.set_title('Survival Rate (The Mechanism)', fontweight='bold')
    ax3.set_ylabel('% Surviving')
    ax3.set_ylim(0, 105)
    ax3.grid(True, alpha=0.3)
    ax3.legend(loc='lower left', fontsize='small')
    
    plt.tight_layout()
    plt.show()

run_phase_1_final_three_panels_v2()

Phase 2: The Summer Slide Economy

import numpy as np
import matplotlib.pyplot as plt

def run_phase_2_random_shocks():
    # Parameters
    n_agents = 5000
    T = 20  # Years
    dt = 1/12 # Monthly steps
    steps = int(T / dt)
    
    W0 = 1000.0
    W_barrier = 200.0
    
    # Common Baseline (Stable Times)
    mu_stable = 0.08
    sigma_stable = 0.25
    
    # Shock Parameters
    shock_prob = 1/12 # Probability per month
    
    # High SES (Buffered) Shock
    mu_shock_H = 0.00
    sigma_shock_H = 0.25
    
    # Low SES (Unbuffered) Shock
    mu_shock_L = -0.20
    sigma_shock_L = 0.45
    
    # Setup Groups
    groups = [
        ("High SES (Buffered)", "green", "-"),
        ("Low SES (Unbuffered)", "red", "--")
    ]
    
    # Data Storage
    results_avg = {g[0]: [W0] for g in groups}
    results_median = {g[0]: [W0] for g in groups}
    results_survival = {g[0]: [1.0] for g in groups}
    
    wealths = {
        groups[0][0]: np.full(n_agents, W0),
        groups[1][0]: np.full(n_agents, W0)
    }
    alives = {
        groups[0][0]: np.ones(n_agents, dtype=bool),
        groups[1][0]: np.ones(n_agents, dtype=bool)
    }
    
    np.random.seed(42)
    
    for t in range(steps):
        for name, color, style in groups:
            w = wealths[name]
            alive = alives[name]
            
            if np.any(alive):
                # Determine who gets shocked (Individual Shocks)
                is_shocked = np.random.rand(n_agents) < shock_prob
                
                # Parameters for each agent
                # Default to stable
                mus = np.full(n_agents, mu_stable)
                sigmas = np.full(n_agents, sigma_stable)
                
                # Apply shock params
                if name == "High SES (Buffered)":
                    mus[is_shocked] = mu_shock_H
                    sigmas[is_shocked] = sigma_shock_H
                else:
                    mus[is_shocked] = mu_shock_L
                    sigmas[is_shocked] = sigma_shock_L
                
                # Geometric Brownian Motion Step
                z = np.random.normal(0, 1, n_agents)
                growth = (mus - 0.5 * sigmas**2) * dt + sigmas * np.sqrt(dt) * z
                
                # Update Wealth
                w[alive] *= np.exp(growth[alive])
                
                # Check Ruin
                ruined = (w < W_barrier) & alive
                alive[ruined] = False
                w[ruined] = 0.0
            
            results_avg[name].append(np.mean(w))
            results_median[name].append(np.median(w))
            results_survival[name].append(np.mean(alive))

    # Plotting
    fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(18, 6))
    time = np.linspace(0, T, steps + 1)
    
    def plot_panel(ax, data_dict, title, ylabel=None, ylim=None):
        for name, color, style in groups:
            y_vals = np.array(data_dict[name]) * 100 if "Survival" in title else data_dict[name]
            ax.plot(time, y_vals, label=name, color=color, linestyle=style, linewidth=2.5)
        
        ax.set_title(title, fontweight='bold')
        if ylabel: ax.set_ylabel(ylabel)
        if ylim: ax.set_ylim(ylim)
        ax.grid(True, alpha=0.3)

    plot_panel(ax1, results_avg, 'Average Wealth', ylabel='Wealth ($)')
    plot_panel(ax2, results_median, 'Median Wealth')
    plot_panel(ax3, results_survival, 'Survival Rate', ylabel='% Surviving', ylim=(0, 105))
    
    # Legend
    lines, labels = ax1.get_legend_handles_labels()
    fig.legend(lines, labels, loc='lower center', ncol=2, bbox_to_anchor=(0.5, -0.05))
    
    plt.tight_layout()
    plt.subplots_adjust(bottom=0.15)
    plt.show()

run_phase_2_random_shocks()

Phase 3: Eating

import numpy as np
import matplotlib.pyplot as plt

def run_phase_3_4pct_unemployment():
    # Parameters
    n_agents = 5000
    T = 100  # 100 Years
    dt = 1/12  # Monthly steps
    steps = int(T / dt)
    
    # Calibration for 4% Unemployment Rate:
    # If duration (D) = 3 months, and prob (p) is monthly:
    # Steady State Rate (u) = p*D / (1 + p*D - p)
    # 0.04 = 3p / (1 + 2p) -> 0.04 + 0.08p = 3p -> 0.04 = 2.92p -> p ≈ 0.0137
    shock_prob = 0.0137 
    unemployment_duration = 3 # months
    
    # Endowments (Latent Heat)
    w_high = np.full(n_agents, 500.0)
    w_low = np.full(n_agents, 50.0)
    
    # Economic Physics
    salary_yr = 55.0
    col_adult_yr = 50.0
    job_penalty = 10.0 # Restart penalty
    
    mu_cap = 0.08    # 8% annual return on savings
    sigma_cap = 0.25 # 25% annual volatility
    
    results = {
        "High SES": {"avg": [], "median": [], "survival": []},
        "Low SES": {"avg": [], "median": [], "survival": []}
    }
    
    np.random.seed(42)
    
    for name, wealth in [("High SES", w_high), ("Low SES", w_low)]:
        w = wealth.copy()
        alive = np.ones(n_agents, dtype=bool)
        unemp_timer = np.zeros(n_agents)
        
        for t in range(steps):
            if np.any(alive):
                # 1. Labor Market Dynamics
                # Check for new shocks
                new_shock = (np.random.rand(n_agents) < shock_prob) & (unemp_timer == 0)
                unemp_timer[new_shock] = unemployment_duration
                
                # Monthly Salary
                monthly_salary = np.full(n_agents, salary_yr * dt)
                monthly_salary[unemp_timer > 0] = 0.0
                
                # Apply Job Penalty at re-employment (end of month 3)
                just_hired = (unemp_timer == 1) 
                w[just_hired & alive] -= job_penalty
                
                # Decrement timer
                unemp_timer[unemp_timer > 0] -= 1
                
                # 2. Capital Growth (Multiplicative)
                z = np.random.normal(0, 1, n_agents)
                growth_mask = (w > 0) & alive
                growth = (mu_cap - 0.5 * sigma_cap**2) * dt + sigma_cap * np.sqrt(dt) * z
                w[growth_mask] *= np.exp(growth[growth_mask])
                
                # 3. Fixed Costs (Eating)
                current_col = col_adult_yr * dt
                w[alive] += monthly_salary[alive] - current_col
                
                # 4. Survival Check
                ruined = (w <= 0) & alive
                alive[ruined] = False
                w[ruined] = 0.0
                
            results[name]["avg"].append(np.mean(w))
            results[name]["median"].append(np.median(w))
            results[name]["survival"].append(np.mean(alive) * 100)

    # Visualization
    fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(20, 6))
    time = np.linspace(0, T, steps)
    
    for name, color, style in [("High SES", "green", "-"), ("Low SES", "red", "--")]:
        ax1.plot(time, results[name]["avg"], label=name, color=color, linestyle=style, linewidth=2)
        ax2.plot(time, results[name]["median"], label=name, color=color, linestyle=style, linewidth=2)
        ax3.plot(time, results[name]["survival"], label=name, color=color, linestyle=style, linewidth=2)
    
    ax1.set_title('Average Wealth (The Illusion)', fontweight='bold')
    ax2.set_title('Median Wealth (The Struggle)', fontweight='bold')
    ax3.set_title('Survival Rate (The Attrition)', fontweight='bold')
    for ax in [ax1, ax2, ax3]:
        ax.grid(True, alpha=0.3)
        ax.legend()
    ax3.set_ylim(0, 105)
    plt.tight_layout()
    plt.show()

run_phase_3_4pct_unemployment()

Phase 4: The Sandwich Generation

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

def run_phase_4_silver_aristocracy():
    # ---------------------------------------------------------
    # 1. Global Parameters (Phase 4 Logic)
    # ---------------------------------------------------------
    n_agents = 5000
    T = 100            # 100-year Post-New Deal Era
    dt = 1/12          # Monthly steps
    steps = int(T / dt)
    np.random.seed(42)
    
    # Endowments (The Inheritance Gap)
    # High SES: $500 (Latent Heat / Buffer)
    # Low SES: $0 (The Sub-Critical Floor)
    w = np.concatenate([np.full(n_agents//2, 500.0), np.full(n_agents//2, 0.0)])
    is_high_ses = np.concatenate([np.ones(n_agents//2, dtype=bool), np.zeros(n_agents//2, dtype=bool)])
    
    # Demographics
    age = np.random.randint(0, 85, n_agents).astype(float)
    unemp_timer = np.zeros(n_agents)
    
    # Child Tracking (Max 3 Children per household)
    n_children = np.zeros(n_agents, dtype=int)
    child_timers = [np.zeros(n_agents) for _ in range(3)] 
    
    # Economic Constants
    salary_yr = 55.0        # Labor Income
    col_adult_yr = 50.0     # Baseline "Eating" Cost
    child_cost_yr = 25.0    # Fixed 50% "Child Tax"
    job_penalty = 10.0      # Restart Haircut (delta)
    parent_threshold = 375.0 # Required wealth (3yrs CoL)
    
    # Market Risk/Return
    mu_cap = 0.08           # 8% "Gifted" Potential
    sigma_cap = 0.25        # 25% "Ferrari" Volatility
    shock_prob = 0.0137     # Calibrated for 4% steady-state unemployment
    unemployment_duration = 3 
    
    # Storage for time-series visualization
    results_median = []
    results_avg = []
    
    # ---------------------------------------------------------
    # 2. Main Simulation Loop
    # ---------------------------------------------------------
    for t in range(steps):
        age += dt
        
        # A. Death & Dynastic Replacement (Inheritance)
        dead = (age >= 85)
        if np.any(dead):
            # The next generation begins - High SES inherit, Low SES reset to 0
            w[dead & is_high_ses] = 500.0
            w[dead & ~is_high_ses] = 0.0
            age[dead] = 0
            n_children[dead] = 0
            for i in range(3): child_timers[i][dead] = 0
            unemp_timer[dead] = 0

        # B. Labor Market (The Effort Bucket)
        is_worker = (age >= 20) & (age < 70)
        is_retiree = (age >= 70) & (age < 85)
        
        # Unemployment shocks
        new_shock = (np.random.rand(n_agents) < shock_prob) & (unemp_timer == 0) & is_worker
        unemp_timer[new_shock] = unemployment_duration
        
        salary_mo = np.zeros(n_agents)
        salary_mo[is_worker & (unemp_timer == 0)] = salary_yr * dt
        
        # Apply Job Penalty (Restart Cost) at the moment of re-employment
        just_hired = (unemp_timer == 1)
        w[just_hired] -= job_penalty
        unemp_timer[unemp_timer > 0] -= 1
        
        # C. The Sandwich Transfer (10% Wage tax from workers to retirees)
        total_transfer_pool = np.sum(salary_mo) * 0.10
        salary_mo[is_worker] *= 0.90 
        
        num_retirees = np.sum(is_retiree)
        if num_retirees > 0:
            w[is_retiree] += (total_transfer_pool / num_retirees)
            
        # D. Capital Growth (The Luck Bucket)
        z = np.random.normal(0, 1, n_agents)
        growth = (mu_cap - 0.5 * sigma_cap**2) * dt + sigma_cap * np.sqrt(dt) * z
        w[w > 0] *= np.exp(growth[w > 0])
        
        # E. The Parent Trap (Reproduction Mechanic)
        can_have_kid = (age >= 25) & (age < 45) & (w >= parent_threshold) & (n_children < 3)
        # Random birth chance if threshold is met
        new_kid = can_have_kid & (np.random.rand(n_agents) < (1/48))
        for i in np.where(new_kid)[0]:
            idx = n_children[i]
            child_timers[idx][i] = 18.0
            n_children[i] += 1
        
        # F. Fixed Drains (Eating + Children)
        active_child_costs = np.zeros(n_agents)
        for i in range(3):
            is_active = child_timers[i] > 0
            active_child_costs[is_active] += (child_cost_yr * dt)
            child_timers[i][is_active] -= dt
        
        w += salary_mo - (col_adult_yr * dt) - active_child_costs
        
        # G. The Ruin Barrier (Volatility Trap)
        w[w < 0.01] = 0.01 
        
        results_median.append(np.median(w))
        results_avg.append(np.mean(w))
        
    # ---------------------------------------------------------
    # 3. Visualization
    # ---------------------------------------------------------
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 7))
    time = np.linspace(0, T, steps)
    
    # Figure 1: Average vs Median
    ax1.plot(time, results_avg, label='Average Wealth (Ensemble)', color='green', linewidth=2)
    ax1.plot(time, results_median, label='Median Wealth (Typical Agent)', color='red', linestyle='--', linewidth=2)
    ax1.set_title('Figure 1: 100-Year Wealth Divergence (Post-New Deal Era)', fontweight='bold')
    ax1.set_xlabel('Years')
    ax1.set_ylabel('Wealth ($)')
    ax1.legend()
    ax1.grid(True, alpha=0.3)
    
    # Figure 2: Log-Quintile Distribution
    retiree_w = w[(age >= 70) & (age < 85)]
    parent_w = w[(age >= 25) & (age < 45)]

    def get_quintile_means(data):
        if len(data) == 0: return [0.01]*5
        data_sorted = np.sort(data)
        chunks = np.array_split(data_sorted, 5)
        return [np.mean(c) for c in chunks]

    p_quint = get_quintile_means(parent_w)
    r_quint = get_quintile_means(retiree_w)
    
    x = np.arange(5)
    width = 0.35
    ax2.bar(x - width/2, p_quint, width, label='Parents (Ages 25-45)', color='navy', alpha=0.8)
    ax2.bar(x + width/2, r_quint, width, label='Retirees (Ages 70-85)', color='darkorange', alpha=0.8)
    ax2.set_title('Figure 2: Wealth Concentration by Quintile (Log Scale)', fontweight='bold')
    ax2.set_xticks(x)
    ax2.set_xticklabels(['Q1', 'Q2', 'Q3', 'Q4', 'Q5'])
    ax2.set_yscale('log')
    ax2.set_ylabel('Wealth ($)')
    ax2.legend()
    ax2.grid(True, which="both", ls="-", alpha=0.2)
    
    plt.tight_layout()
    plt.show()

# Execution
run_phase_4_silver_aristocracy()

Phase 5: Tax Policy

import numpy as np
import matplotlib.pyplot as plt

def run_comparative_tax_simulation():
    # ---------------------------------------------------------
    # 1. Global Parameters
    # ---------------------------------------------------------
    n_agents = 5000
    T = 100            # 100 Years
    dt = 1/12          # Monthly time steps
    steps = int(T / dt)
    
    # Common Economic Parameters
    salary_yr = 55.0        
    col_adult_yr = 50.0     
    child_cost_yr = 25.0    
    job_penalty = 10.0      
    parent_threshold = 375.0 
    mu_cap = 0.08           # 8% Return (The "Ferrari")
    sigma_cap = 0.25        # 25% Volatility
    shock_prob = 0.0137     # 4% Unemployment Rate
    unemployment_duration = 3 
    
    # The Two Scenarios
    scenarios = [
        {
            "name": "Scenario A: Annual Tax (30% Cap / 0% Estate)", 
            "labor_tax": 0.10, 
            "annual_cap_tax": 0.30, 
            "estate_tax": 0.0
        },
        {
            "name": "Scenario B: Deferred Tax (0% Cap / 50% Estate)", 
            "labor_tax": 0.10, 
            "annual_cap_tax": 0.0, 
            "estate_tax": 0.50
        }
    ]
    
    results = {}

    print(f"Running Simulation for {T} Years with N={n_agents} Agents...")

    for sc in scenarios:
        # Reset Agents
        # High SES start at $500; Low SES start at $0
        w = np.concatenate([np.full(n_agents//2, 500.0), np.full(n_agents//2, 0.0)])
        is_high_ses = np.concatenate([np.ones(n_agents//2, dtype=bool), np.zeros(n_agents//2, dtype=bool)])
        
        # Consistent demographics reset
        np.random.seed(42) 
        age = np.random.randint(0, 85, n_agents).astype(float)
        unemp_timer = np.zeros(n_agents)
        n_children = np.zeros(n_agents, dtype=int)
        child_timers = [np.zeros(n_agents) for _ in range(3)] 
        
        median_history = []
        aggregate_history = []
        
        # Re-seed for loop dynamics
        np.random.seed(42) 

        for t in range(steps):
            age += dt
            
            # --- DEATH & ESTATE TAX (The Harvest) ---
            dead = (age >= 85)
            estate_tax_revenue = 0.0
            
            if np.any(dead):
                dead_wealth = w[dead].copy()
                
                # Apply Estate Tax
                tax_collected = dead_wealth * sc["estate_tax"]
                estate_tax_revenue = np.sum(tax_collected)
                
                # Inheritance (Heir gets the remainder)
                w[dead] -= tax_collected
                
                # Reset Demographics for Heir
                age[dead] = 0
                n_children[dead] = 0
                for i in range(3): child_timers[i][dead] = 0
                unemp_timer[dead] = 0

            # --- LABOR MARKET ---
            is_worker = (age >= 20) & (age < 70)
            
            new_shock = (np.random.rand(n_agents) < shock_prob) & (unemp_timer == 0) & is_worker
            unemp_timer[new_shock] = unemployment_duration
            
            salary_mo = np.zeros(n_agents)
            salary_mo[is_worker & (unemp_timer == 0)] = salary_yr * dt
            
            just_hired = (unemp_timer == 1)
            w[just_hired] -= job_penalty
            unemp_timer[unemp_timer > 0] -= 1
            
            # --- LABOR TAX ---
            tax_labor_rev = np.sum(salary_mo * sc["labor_tax"])
            salary_mo *= (1 - sc["labor_tax"])
            
            # --- CAPITAL GROWTH & ANNUAL TAX ---
            current_w = w.copy()
            z = np.random.normal(0, 1, n_agents)
            growth = (mu_cap - 0.5 * sigma_cap**2) * dt + sigma_cap * np.sqrt(dt) * z
            w[w > 0] *= np.exp(growth[w > 0])
            
            # Annual Capital Tax (Only for Scenario A)
            annual_tax_rev = 0.0
            if sc["annual_cap_tax"] > 0:
                gains = w - current_w
                taxable_gains = np.maximum(gains, 0)
                tax_collected = taxable_gains * sc["annual_cap_tax"]
                annual_tax_rev = np.sum(tax_collected)
                w -= tax_collected
            
            # --- DIVIDEND (The Mechanism) ---
            # Pool all three sources: Labor Tax + Annual Cap Tax + Estate Tax
            total_revenue = tax_labor_rev + annual_tax_rev + estate_tax_revenue
            dividend = total_revenue / n_agents
            w += dividend
            
            # --- COSTS ---
            active_child_costs = np.zeros(n_agents)
            for i in range(3):
                is_active = child_timers[i] > 0
                active_child_costs[is_active] += (child_cost_yr * dt)
                child_timers[i][is_active] -= dt
            
            w += salary_mo - (col_adult_yr * dt) - active_child_costs
            
            # Floor (Absorbing Barrier)
            w[w < 0.01] = 0.01 
            
            # Reproduction
            can_have_kid = (age >= 25) & (age < 45) & (w >= parent_threshold) & (n_children < 3)
            new_kid = can_have_kid & (np.random.rand(n_agents) < (1/48))
            for i in np.where(new_kid)[0]:
                idx = n_children[i]
                child_timers[idx][i] = 18.0
                n_children[i] += 1
            
            median_history.append(np.median(w))
            aggregate_history.append(np.sum(w))
            
        results[sc["name"]] = {"median": median_history, "aggregate": aggregate_history}

    # ---------------------------------------------------------
    # 2. Visualization
    # ---------------------------------------------------------
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 6))
    time = np.linspace(0, T, steps)
    
    # Plot Aggregate Wealth
    for name, data in results.items():
        color = 'red' if "Annual" in name else 'green'
        style = '--' if "Annual" in name else '-'
        ax1.plot(time, data["aggregate"], label=name, color=color, linestyle=style, linewidth=2)
    
    ax1.set_title('Figure 1: Aggregate Wealth (The Pie)', fontweight='bold')
    ax1.set_ylabel('Total Wealth ($) - Log Scale')
    ax1.set_xlabel('Years')
    ax1.set_yscale('log') # Log scale needed to see the magnitude difference
    ax1.legend()
    ax1.grid(True, which="both", ls="-", alpha=0.2)
    
    # Plot Median Wealth
    for name, data in results.items():
        color = 'red' if "Annual" in name else 'green'
        style = '--' if "Annual" in name else '-'
        ax2.plot(time, data["median"], label=name, color=color, linestyle=style, linewidth=2)
    
    ax2.set_title('Figure 2: Median Wealth (The Household)', fontweight='bold')
    ax2.set_ylabel('Median Wealth ($)')
    ax2.set_xlabel('Years')
    ax2.legend()
    ax2.grid(True, alpha=0.3)
    
    plt.tight_layout()
    plt.show()
    
    # Print Final Stats
    print("\nFinal Statistics (Year 100):")
    for name, data in results.items():
        print(f"{name}:")
        print(f"  Aggregate Wealth: ${data['aggregate'][-1]:,.0f}")
        print(f"  Median Wealth:    ${data['median'][-1]:,.0f}")
        print("-" * 30)

run_comparative_tax_simulation 

Keep reading with a 7-day free trial

Subscribe to Yes, I give a fig... thoughts on markets from Michael Green to keep reading this post and get 7 days of free access to the full post archives.

Already a paid subscriber? Sign in
© 2026 Michael W. Green · Privacy ∙ Terms ∙ Collection notice
Start your SubstackGet the app
Substack is the home for great culture