Skip to content

Instantly share code, notes, and snippets.

@thuliumsystems
Last active May 23, 2025 00:23
Show Gist options
  • Select an option

  • Save thuliumsystems/5a9422f4269e94300c6101ff70ab7282 to your computer and use it in GitHub Desktop.

Select an option

Save thuliumsystems/5a9422f4269e94300c6101ff70ab7282 to your computer and use it in GitHub Desktop.
Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"cell_type": "code",
"execution_count": 27,
"id": "9612a570",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"--- TSP Balance Over Time ---\n",
" Age Aggressive (8% Annual) Conservative (4% Annual)\n",
"Date \n",
"2025-07 20 $1,503.15 $1,503.15\n",
"2025-07 20 $3,015.97 $3,011.22\n",
"2025-08 21 $4,538.53 $4,524.23\n",
"2025-09 21 $6,070.88 $6,042.19\n",
"2025-10 21 $7,613.09 $7,565.12\n",
"2025-11 21 $9,165.22 $9,093.04\n",
"2025-12 21 $10,727.34 $10,625.96\n",
"2026 22 $31,023.12 $30,151.79\n",
"2027 23 $39,997.37 $37,856.49\n",
"2028 24 $51,724.57 $47,593.61\n",
"2029 25 $63,746.32 $57,244.53\n",
"2030 26 $77,001.66 $67,548.64\n",
"2031 27 $90,330.31 $77,600.22\n",
"2032 28 $107,615.77 $90,221.94\n",
"2033 29 $125,196.23 $102,646.57\n",
"2034 30 $144,454.98 $115,835.33\n",
"2035 31 $163,707.74 $128,632.43\n",
"2036 32 $188,555.15 $144,628.72\n",
"2037 33 $213,698.18 $160,298.19\n",
"2038 34 $241,124.50 $176,861.59\n",
"2039 35 $268,436.96 $192,870.57\n",
"2040 36 $303,572.31 $212,814.62\n",
"2041 37 $339,004.13 $232,280.11\n",
"2042 38 $377,542.35 $252,791.36\n",
"2043 39 $415,819.93 $272,557.78\n",
"2044 40 $464,951.92 $297,120.16\n",
"2045 41 $514,381.52 $321,026.44\n",
"2046 42 $568,037.35 $346,156.13\n",
"2047 43 $621,232.86 $370,318.21\n",
"2048 44 $689,407.12 $400,283.38\n",
"2049 45 $757,880.56 $429,384.77\n",
"2050 46 $832,103.73 $459,917.36\n",
"2051 47 $905,594.91 $489,221.75\n",
"2052 48 $999,675.96 $525,507.43\n",
"2053 49 $1,094,058.33 $560,686.36\n",
"2054 50 $1,196,263.14 $597,539.59\n",
"2055 51 $1,297,366.37 $632,859.75\n",
"2056 52 $1,426,693.33 $676,539.52\n",
"2057 53 $1,556,324.51 $718,828.31\n",
"2058 54 $1,696,598.03 $763,075.80\n",
"2059 55 $1,835,267.14 $805,433.57\n",
"2060 56 $2,012,545.78 $857,763.38\n",
"2061 57 $2,190,132.56 $908,369.70\n",
"2062 58 $2,382,198.15 $961,267.42\n",
"2063 59 $2,571,975.23 $1,011,858.20\n",
"\n",
"--- Annual Salary Over Time ---\n",
" Annual Salary\n",
"Year \n",
"2025 $32,796.00\n",
"2026 $34,107.84\n",
"2027 $35,419.68\n",
"2028 $36,731.52\n",
"2029 $38,043.36\n",
"2030 $39,355.20\n",
"2031 $40,667.04\n",
"2032 $41,978.88\n",
"2033 $43,290.72\n",
"2034 $44,602.56\n",
"2035 $45,914.40\n",
"2036 $47,226.24\n",
"2037 $48,538.08\n",
"2038 $49,849.92\n",
"2039 $51,161.76\n",
"2040 $52,473.60\n",
"2041 $53,785.44\n",
"2042 $55,097.28\n",
"2043 $56,409.12\n",
"2044 $57,720.96\n",
"2045 $59,032.80\n",
"2046 $60,344.64\n",
"2047 $61,656.48\n",
"2048 $62,968.32\n",
"2049 $64,280.16\n",
"2050 $65,592.00\n",
"2051 $66,903.84\n",
"2052 $68,215.68\n",
"2053 $69,527.52\n",
"2054 $70,839.36\n",
"2055 $72,151.20\n",
"2056 $73,463.04\n",
"2057 $74,774.88\n",
"2058 $76,086.72\n",
"2059 $77,398.56\n",
"2060 $78,710.40\n",
"2061 $80,022.24\n",
"2062 $81,334.08\n",
"2063 $82,645.92\n",
"2064 $83,957.76\n"
]
}
],
"source": [
"from datetime import datetime, timedelta\n",
"import pandas as pd\n",
"\n",
"# Sets the current date for the simulation context\n",
"current_date = datetime(2025, 7, 1)\n",
"# Stores the user's birth date as a datetime object.\n",
"birth_date = datetime.strptime(\"2004-08-02\", \"%Y-%m-%d\")\n",
"\n",
"# Specifies the number of initial years during which a higher contribution rate will apply.\n",
"initial_years_high_contribution = 2\n",
"# Sets the contribution rate (as a percentage) for the initial period.\n",
"initial_contribution_rate = 0.5\n",
"\n",
"# Sets the contribution rate (as a percentage) for the period after the initial years, until retirement.\n",
"later_contribution_rate = 0.15\n",
"\n",
"# Defines the target retirement age in years.\n",
"retirement_age = 59.5\n",
"# Sets the annual contribution limit for the Roth TSP in 2025.\n",
"annual_limit = 23000\n",
"# Defines the initial base monthly salary (e.g., E-3 salary as of April 2025).\n",
"salary_start = 2733.00\n",
"# Sets the employer (DoD) matching contribution rate as a percentage of salary.\n",
"match_rate = 0.05\n",
"# Annual real growth rate for salary (applied without compounding to the base salary).\n",
"salary_growth_rate_annual = 0.04\n",
"\n",
"# Investment Parameters (adjusted for effective annual compounding)\n",
"# Monthly growth rate equivalent to 8% annual growth, compounded monthly.\n",
"growth_aggressive = (1 + 0.08)**(1/12) - 1\n",
"# Monthly growth rate equivalent to 4% annual growth, compounded monthly.\n",
"growth_conservative = (1 + 0.04)**(1/12) - 1\n",
"\n",
"# --- Date Calculations for Simulation ---\n",
"# The simulation starts from the current_date.\n",
"start_sim_date = current_date\n",
"\n",
"# Calculates the exact retirement date (59 years and 6 months after birth date).\n",
"target_year = birth_date.year + int(retirement_age)\n",
"target_month = birth_date.month + int((retirement_age - int(retirement_age)) * 12)\n",
"if target_month > 12:\n",
" target_year += (target_month - 1) // 12\n",
" target_month = (target_month - 1) % 12 + 1\n",
"retirement_date = datetime(target_year, target_month, birth_date.day)\n",
"\n",
"# Calculates the total number of months for the simulation.\n",
"# The simulation runs from the current date until the retirement date.\n",
"total_months_simulation = int((retirement_date - start_sim_date).days / 30.4375) + 1\n",
"if total_months_simulation < 0:\n",
" total_months_simulation = 0 # If retirement date has already passed\n",
"\n",
"# --- Function to Simulate TSP Growth ---\n",
"def simulate_tsp_custom(initial_monthly_salary, growth_rate, initial_years_high_contribution, initial_rate, later_rate, total_months_sim, start_sim_date, birth_date, annual_limit, match_rate, salary_growth_rate_annual):\n",
" balance = 0\n",
" balances = []\n",
" # Store the initial monthly salary for simple growth calculation\n",
" base_monthly_salary = initial_monthly_salary\n",
"\n",
" for month_offset in range(total_months_sim):\n",
" # Calculate the current date within the simulation loop\n",
" current_sim_date = start_sim_date + timedelta(days=month_offset * 30.4375)\n",
"\n",
" # Calculate years passed since the simulation start for salary increase and contribution rate\n",
" # Using full years passed since the start_sim_date to apply simple salary growth annually.\n",
" sim_years_passed = (current_sim_date.year - start_sim_date.year)\n",
"\n",
" # Calculate current monthly salary with simple annual growth\n",
" # Each year, the salary increases by a fixed amount based on the initial salary (not compounded).\n",
" current_monthly_salary = base_monthly_salary + (sim_years_passed * (base_monthly_salary * salary_growth_rate_annual))\n",
"\n",
" if sim_years_passed < initial_years_high_contribution:\n",
" personal_contribution = current_monthly_salary * initial_rate\n",
" else:\n",
" personal_contribution = min(current_monthly_salary * later_rate, annual_limit / 12)\n",
"\n",
" # Calculate DoD matching contribution\n",
" dod_match = min(current_monthly_salary * match_rate, personal_contribution)\n",
" # Total contribution for the month\n",
" total_contribution = personal_contribution + dod_match\n",
"\n",
" # Update balance with growth and new contribution\n",
" balance = balance * (1 + growth_rate) + total_contribution\n",
" balances.append(balance)\n",
" return balances\n",
"\n",
"# --- Simulate Annual Salary Growth (without compounding) ---\n",
"def simulate_annual_salary_growth(start_salary_monthly, annual_growth_rate, start_date, end_date):\n",
" salary_data = []\n",
" base_monthly_salary = start_salary_monthly # Store the initial base salary for non-compounding growth\n",
" \n",
" # Iterate through years from the start_date's year up to the end_date's year\n",
" for year in range(start_date.year, end_date.year + 1):\n",
" years_passed = year - start_date.year\n",
" # Calculate current monthly salary with simple annual growth\n",
" # The increase is a fixed amount (base_monthly_salary * annual_growth_rate) added each year.\n",
" current_monthly_salary = base_monthly_salary + (years_passed * (base_monthly_salary * annual_growth_rate))\n",
" \n",
" salary_data.append({\n",
" \"Year\": year,\n",
" \"Annual Salary\": current_monthly_salary * 12 # Annualized salary for the year\n",
" })\n",
" return pd.DataFrame(salary_data).set_index(\"Year\")\n",
"\n",
"# --- Run TSP Simulations ---\n",
"balances_aggressive = simulate_tsp_custom(\n",
" salary_start, growth_aggressive, initial_years_high_contribution, initial_contribution_rate,\n",
" later_contribution_rate, total_months_simulation, start_sim_date, birth_date, annual_limit, match_rate, salary_growth_rate_annual\n",
")\n",
"balances_conservative = simulate_tsp_custom(\n",
" salary_start, growth_conservative, initial_years_high_contribution, initial_contribution_rate,\n",
" later_contribution_rate, total_months_simulation, start_sim_date, birth_date, annual_limit, match_rate, salary_growth_rate_annual\n",
")\n",
"\n",
"# --- Prepare Data for TSP DataFrame ---\n",
"data_for_df = []\n",
"current_sim_month_index = 0\n",
"while current_sim_month_index < total_months_simulation:\n",
" sim_date = start_sim_date + timedelta(days=current_sim_month_index * 30.4375)\n",
"\n",
" if sim_date.year == 2025 and sim_date.month <= 12:\n",
" age_at_month = (sim_date - birth_date).days / 365.25\n",
" if current_sim_month_index < len(balances_aggressive):\n",
" data_for_df.append({\n",
" \"Date\": sim_date.strftime(\"%Y-%m\"),\n",
" \"Age\": int(age_at_month),\n",
" \"Aggressive (8% Annual)\": balances_aggressive[current_sim_month_index],\n",
" \"Conservative (4% Annual)\": balances_conservative[current_sim_month_index]\n",
" })\n",
" current_sim_month_index += 1\n",
" elif sim_date.year > 2025:\n",
" break\n",
"\n",
"start_yearly_data_year = 2026\n",
"for year_val in range(start_yearly_data_year, retirement_date.year + 1):\n",
" target_date_for_year_end = datetime(year_val, 12, 31)\n",
" months_offset_to_year_end = int((target_date_for_year_end - start_sim_date).days / 30.4375)\n",
"\n",
" if months_offset_to_year_end >= 0 and months_offset_to_year_end < total_months_simulation:\n",
" age_at_year_end = (target_date_for_year_end - birth_date).days / 365.25\n",
" data_for_df.append({\n",
" \"Date\": str(year_val),\n",
" \"Age\": int(age_at_year_end),\n",
" \"Aggressive (8% Annual)\": balances_aggressive[months_offset_to_year_end],\n",
" \"Conservative (4% Annual)\": balances_conservative[months_offset_to_year_end]\n",
" })\n",
"\n",
"df_tsp = pd.DataFrame(data_for_df)\n",
"df_tsp.set_index(\"Date\", inplace=True)\n",
"df_tsp[\"Aggressive (8% Annual)\"] = df_tsp[\"Aggressive (8% Annual)\"].map(\"${:,.2f}\".format)\n",
"df_tsp[\"Conservative (4% Annual)\"] = df_tsp[\"Conservative (4% Annual)\"].map(\"${:,.2f}\".format)\n",
"\n",
"print(\"\\n--- TSP Balance Over Time ---\")\n",
"print(df_tsp)\n",
"\n",
"df_salary = simulate_annual_salary_growth(salary_start, salary_growth_rate_annual, start_sim_date, retirement_date)\n",
"df_salary[\"Annual Salary\"] = df_salary[\"Annual Salary\"].map(\"${:,.2f}\".format)\n",
"\n",
"print(\"\\n--- Annual Salary Over Time ---\")\n",
"print(df_salary)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.9.6"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment