Version 1.1

This commit is contained in:
2026-02-18 08:24:54 -05:00
parent 4318c8f642
commit fdba869a8d
33 changed files with 2142 additions and 1942 deletions

View File

@@ -1,195 +1,344 @@
"""
Lottery Investment Calculator
Handles both US and Canadian lottery calculations
Lottery Investment Calculator — pure calculation logic.
All functions are deterministic and side-effect free.
Tax rates, exchange rates, and investment defaults are passed as explicit
parameters (with sensible defaults) so that callers can override via config.
"""
def calculate_us_lottery(jackpot, invest_percentage=0.90, annual_return=0.045, cycles=8):
from __future__ import annotations
# ---------------------------------------------------------------------------
# Core calculators
# ---------------------------------------------------------------------------
def _run_investment_cycles(
principal: float,
annual_return: float,
cycles: int,
investment_income_tax_rate: float,
personal_withdrawal_pct: float,
) -> tuple[list[dict], float, float]:
"""Simulate 90-day reinvestment cycles.
Returns:
(cycle_results, total_personal_withdrawals, final_principal)
"""
Calculate investment returns for US lottery winnings
cycle_results: list[dict] = []
total_personal_withdrawals = 0.0
for cycle in range(1, cycles + 1):
interest_earned = principal * annual_return * (90 / 365)
taxes_owed = interest_earned * investment_income_tax_rate
personal_withdrawal = interest_earned * personal_withdrawal_pct
total_withdrawal = taxes_owed + personal_withdrawal
reinvestment = interest_earned - total_withdrawal
new_principal = principal + reinvestment
total_personal_withdrawals += personal_withdrawal
cycle_results.append(
{
"cycle": cycle,
"principalStart": principal,
"interestEarned": interest_earned,
"taxesOwed": taxes_owed,
"personalWithdrawal": personal_withdrawal,
"totalWithdrawal": total_withdrawal,
"reinvestment": reinvestment,
"principalEnd": new_principal,
}
)
principal = new_principal
return cycle_results, total_personal_withdrawals, principal
def calculate_us_lottery(
jackpot: float,
invest_percentage: float = 0.90,
annual_return: float = 0.045,
cycles: int = 8,
*,
state_tax_rate: float = 0.055,
lump_sum_rate: float = 0.52,
federal_tax_rate: float = 0.37,
usd_cad_rate: float = 1.35,
investment_income_tax_rate: float = 0.5353,
personal_withdrawal_pct: float = 0.10,
) -> dict:
"""Calculate investment returns for US lottery winnings.
Args:
jackpot: Original jackpot amount (USD)
invest_percentage: Percentage to invest (default 90%)
annual_return: Annual return rate (default 4.5%)
cycles: Number of 90-day cycles to calculate (default 8)
jackpot: Advertised jackpot amount (USD).
invest_percentage: Fraction to invest (0-1).
annual_return: Expected annual return rate.
cycles: Number of 90-day reinvestment cycles.
state_tax_rate: State income-tax rate on winnings.
lump_sum_rate: Fraction of advertised jackpot available as lump sum.
federal_tax_rate: Federal income-tax rate on winnings.
usd_cad_rate: USD to CAD exchange rate.
investment_income_tax_rate: Marginal tax on investment income.
personal_withdrawal_pct: Fraction of interest withdrawn each cycle.
"""
# US Lottery calculations
cash_sum = jackpot * 0.52 # Lump sum is 52%
federal_tax = cash_sum * 0.37
state_tax = cash_sum * 0.055
cash_sum = jackpot * lump_sum_rate
federal_tax = cash_sum * federal_tax_rate
state_tax = cash_sum * state_tax_rate
net_amount = cash_sum - federal_tax - state_tax
# Convert to Canadian dollars
canadian_amount = net_amount * 1.35
# Split into investment and fun money
canadian_amount = net_amount * usd_cad_rate
investment_principal = canadian_amount * invest_percentage
fun_money = canadian_amount * (1 - invest_percentage)
# Calculate cycles
cycle_results = []
principal = investment_principal
total_personal_withdrawals = 0
for cycle in range(1, cycles + 1):
# Interest for 90 days
interest_earned = principal * annual_return * (90/365)
# Taxes on investment income (53.53%)
taxes_owed = interest_earned * 0.5353
# Personal withdrawal (10% of interest)
personal_withdrawal = interest_earned * 0.10
# Total withdrawal
total_withdrawal = taxes_owed + personal_withdrawal
# Reinvestment
reinvestment = interest_earned - total_withdrawal
# New principal
new_principal = principal + reinvestment
total_personal_withdrawals += personal_withdrawal
cycle_results.append({
'cycle': cycle,
'principal_start': principal,
'interest_earned': interest_earned,
'taxes_owed': taxes_owed,
'personal_withdrawal': personal_withdrawal,
'total_withdrawal': total_withdrawal,
'reinvestment': reinvestment,
'principal_end': new_principal
})
principal = new_principal
# Calculate daily income
net_daily_income = (investment_principal * annual_return * 0.5353) / 365
cycle_results, total_withdrawals, final_principal = _run_investment_cycles(
investment_principal,
annual_return,
cycles,
investment_income_tax_rate,
personal_withdrawal_pct,
)
net_daily_income = (investment_principal * annual_return * (1 - investment_income_tax_rate)) / 365
return {
'country': 'US',
'original_jackpot': jackpot,
'cash_sum': cash_sum,
'federal_tax': federal_tax,
'state_tax': state_tax,
'net_amount_usd': net_amount,
'net_amount_cad': canadian_amount,
'investment_principal': investment_principal,
'fun_money': fun_money,
'net_daily_income': net_daily_income,
'annual_income': net_daily_income * 365,
'total_personal_withdrawals': total_personal_withdrawals,
'final_principal': principal,
'cycles': cycle_results
"country": "US",
"originalJackpot": jackpot,
"cashSum": cash_sum,
"federalTax": federal_tax,
"stateTax": state_tax,
"stateTaxRate": state_tax_rate,
"netAmountUsd": net_amount,
"netAmountCad": canadian_amount,
"investmentPrincipal": investment_principal,
"funMoney": fun_money,
"netDailyIncome": net_daily_income,
"annualIncome": net_daily_income * 365,
"totalPersonalWithdrawals": total_withdrawals,
"finalPrincipal": final_principal,
"cycles": cycle_results,
}
def calculate_canadian_lottery(jackpot, invest_percentage=0.90, annual_return=0.045, cycles=8):
"""
Calculate investment returns for Canadian lottery winnings
def calculate_canadian_lottery(
jackpot: float,
invest_percentage: float = 0.90,
annual_return: float = 0.045,
cycles: int = 8,
*,
investment_income_tax_rate: float = 0.5353,
personal_withdrawal_pct: float = 0.10,
) -> dict:
"""Calculate investment returns for Canadian lottery winnings (tax-free).
Args:
jackpot: Original jackpot amount (CAD) - TAX FREE!
invest_percentage: Percentage to invest (default 90%)
annual_return: Annual return rate (default 4.5%)
cycles: Number of 90-day cycles to calculate (default 8)
jackpot: Jackpot amount (CAD) - no tax deducted on winnings.
invest_percentage: Fraction to invest (0-1).
annual_return: Expected annual return rate.
cycles: Number of 90-day reinvestment cycles.
investment_income_tax_rate: Marginal tax on investment income.
personal_withdrawal_pct: Fraction of interest withdrawn each cycle.
"""
# Canadian lotteries - NO TAX on winnings!
net_amount = jackpot
# Split into investment and fun money
net_amount = jackpot # Tax-free!
investment_principal = net_amount * invest_percentage
fun_money = net_amount * (1 - invest_percentage)
# Calculate cycles
cycle_results = []
principal = investment_principal
total_personal_withdrawals = 0
for cycle in range(1, cycles + 1):
# Interest for 90 days
interest_earned = principal * annual_return * (90/365)
# Taxes on investment income (53.53%)
taxes_owed = interest_earned * 0.5353
# Personal withdrawal (10% of interest)
personal_withdrawal = interest_earned * 0.10
# Total withdrawal
total_withdrawal = taxes_owed + personal_withdrawal
# Reinvestment
reinvestment = interest_earned - total_withdrawal
# New principal
new_principal = principal + reinvestment
total_personal_withdrawals += personal_withdrawal
cycle_results.append({
'cycle': cycle,
'principal_start': principal,
'interest_earned': interest_earned,
'taxes_owed': taxes_owed,
'personal_withdrawal': personal_withdrawal,
'total_withdrawal': total_withdrawal,
'reinvestment': reinvestment,
'principal_end': new_principal
})
principal = new_principal
# Calculate daily income
net_daily_income = (investment_principal * annual_return * 0.5353) / 365
cycle_results, total_withdrawals, final_principal = _run_investment_cycles(
investment_principal,
annual_return,
cycles,
investment_income_tax_rate,
personal_withdrawal_pct,
)
net_daily_income = (investment_principal * annual_return * (1 - investment_income_tax_rate)) / 365
return {
'country': 'Canada',
'original_jackpot': jackpot,
'net_amount_cad': net_amount,
'investment_principal': investment_principal,
'fun_money': fun_money,
'net_daily_income': net_daily_income,
'annual_income': net_daily_income * 365,
'total_personal_withdrawals': total_personal_withdrawals,
'final_principal': principal,
'cycles': cycle_results
"country": "Canada",
"originalJackpot": jackpot,
"netAmountCad": net_amount,
"investmentPrincipal": investment_principal,
"funMoney": fun_money,
"netDailyIncome": net_daily_income,
"annualIncome": net_daily_income * 365,
"totalPersonalWithdrawals": total_withdrawals,
"finalPrincipal": final_principal,
"cycles": cycle_results,
}
if __name__ == "__main__":
# Test with current jackpots
print("=" * 80)
print("US LOTTERY - MEGA MILLIONS ($547M)")
print("=" * 80)
us_result = calculate_us_lottery(547_000_000)
print(f"Original Jackpot: ${us_result['original_jackpot']:,.0f}")
print(f"Cash Sum (52%): ${us_result['cash_sum']:,.0f}")
print(f"After Taxes (USD): ${us_result['net_amount_usd']:,.0f}")
print(f"After Taxes (CAD): ${us_result['net_amount_cad']:,.0f}")
print(f"Investment (90%): ${us_result['investment_principal']:,.0f}")
print(f"Fun Money (10%): ${us_result['fun_money']:,.0f}")
print(f"Daily Income: ${us_result['net_daily_income']:,.2f}")
print(f"Annual Income: ${us_result['annual_income']:,.2f}")
print(f"Final Principal (after 8 cycles): ${us_result['final_principal']:,.0f}")
print("\n" + "=" * 80)
print("CANADIAN LOTTERY - LOTTO 6/49 ($32M CAD)")
print("=" * 80)
can_result = calculate_canadian_lottery(32_000_000)
print(f"Original Jackpot (TAX FREE!): ${can_result['original_jackpot']:,.0f}")
print(f"Investment (90%): ${can_result['investment_principal']:,.0f}")
print(f"Fun Money (10%): ${can_result['fun_money']:,.0f}")
print(f"Daily Income: ${can_result['net_daily_income']:,.2f}")
print(f"Annual Income: ${can_result['annual_income']:,.2f}")
print(f"Final Principal (after 8 cycles): ${can_result['final_principal']:,.0f}")
print("\n" + "=" * 80)
print("COMPARISON")
print("=" * 80)
print(f"US ($547M) - You keep: ${us_result['net_amount_cad']:,.0f} CAD after taxes")
print(f"Canadian ($32M) - You keep: ${can_result['net_amount_cad']:,.0f} CAD (NO TAXES!)")
print(f"\nUS Daily Income: ${us_result['net_daily_income']:,.2f}")
print(f"Canadian Daily Income: ${can_result['net_daily_income']:,.2f}")
# ---------------------------------------------------------------------------
# Break-even calculator
# ---------------------------------------------------------------------------
def calculate_break_even(
odds: int,
ticket_cost: float,
country: str = "us",
*,
lump_sum_rate: float = 0.52,
federal_tax_rate: float = 0.37,
state_tax_rate: float = 0.055,
) -> dict:
"""Calculate the jackpot where expected value >= ticket cost.
For US lotteries the take-home fraction is::
lump_sum_rate * (1 - federal_tax_rate - state_tax_rate)
For Canadian lotteries the full jackpot is kept (tax-free).
Returns a dict with the break-even jackpot and supporting details.
"""
if country == "us":
take_home_fraction = lump_sum_rate * (1 - federal_tax_rate - state_tax_rate)
else:
take_home_fraction = 1.0
# EV = (jackpot * take_home_fraction) / odds >= ticket_cost
# => jackpot >= ticket_cost * odds / take_home_fraction
break_even_jackpot = (ticket_cost * odds) / take_home_fraction
probability = 1 / odds
return {
"breakEvenJackpot": break_even_jackpot,
"takeHomeFraction": take_home_fraction,
"odds": odds,
"probability": probability,
"ticketCost": ticket_cost,
"expectedValueAtBreakEven": probability * break_even_jackpot * take_home_fraction,
}
# ---------------------------------------------------------------------------
# Annuity calculator
# ---------------------------------------------------------------------------
def calculate_annuity(
jackpot: float,
country: str = "us",
years: int = 30,
annual_increase: float = 0.05,
*,
federal_tax_rate: float = 0.37,
state_tax_rate: float = 0.055,
) -> dict:
"""Calculate a multi-year annuity payout schedule.
Powerball / Mega Millions annuities pay an initial amount then increase
each year by *annual_increase* (typically 5%).
Returns yearly pre-tax and after-tax amounts plus totals.
"""
# Calculate initial annual payment using geometric series:
# jackpot = payment * sum((1 + r)^k) for k=0..years-1
# = payment * ((1+r)^years - 1) / r
if annual_increase > 0:
geo_sum = ((1 + annual_increase) ** years - 1) / annual_increase
else:
geo_sum = float(years)
initial_payment = jackpot / geo_sum
schedule: list[dict] = []
total_pre_tax = 0.0
total_after_tax = 0.0
for year in range(1, years + 1):
pre_tax = initial_payment * (1 + annual_increase) ** (year - 1)
tax = pre_tax * (federal_tax_rate + state_tax_rate) if country == "us" else 0.0
after_tax = pre_tax - tax
total_pre_tax += pre_tax
total_after_tax += after_tax
schedule.append(
{
"year": year,
"preTax": pre_tax,
"tax": tax,
"afterTax": after_tax,
}
)
return {
"jackpot": jackpot,
"country": country,
"years": years,
"annualIncrease": annual_increase,
"initialPayment": initial_payment,
"totalPreTax": total_pre_tax,
"totalAfterTax": total_after_tax,
"schedule": schedule,
}
# ---------------------------------------------------------------------------
# Group play calculator
# ---------------------------------------------------------------------------
def calculate_group_split(
jackpot: float,
members: int = 2,
shares: list[float] | None = None,
country: str = "us",
*,
lump_sum_rate: float = 0.52,
federal_tax_rate: float = 0.37,
state_tax_rate: float = 0.055,
usd_cad_rate: float = 1.35,
investment_income_tax_rate: float = 0.5353,
personal_withdrawal_pct: float = 0.10,
) -> dict:
"""Split lottery winnings among *members* with optional custom shares.
If *shares* is None every member gets an equal share. Otherwise *shares*
must be a list of floats summing to ~1.0 with length == *members*.
"""
# Normalise shares
if shares is None:
share_list = [1.0 / members] * members
else:
if len(shares) != members:
share_list = [1.0 / members] * members
else:
total = sum(shares)
share_list = [s / total for s in shares] if total > 0 else [1.0 / members] * members
member_results: list[dict] = []
for i, share in enumerate(share_list):
member_jackpot = jackpot * share
if country == "us":
calc = calculate_us_lottery(
member_jackpot,
lump_sum_rate=lump_sum_rate,
federal_tax_rate=federal_tax_rate,
state_tax_rate=state_tax_rate,
usd_cad_rate=usd_cad_rate,
investment_income_tax_rate=investment_income_tax_rate,
personal_withdrawal_pct=personal_withdrawal_pct,
)
else:
calc = calculate_canadian_lottery(
member_jackpot,
investment_income_tax_rate=investment_income_tax_rate,
personal_withdrawal_pct=personal_withdrawal_pct,
)
member_results.append(
{
"member": i + 1,
"share": share,
"jackpotShare": member_jackpot,
"calculation": calc,
}
)
return {
"originalJackpot": jackpot,
"members": members,
"shares": share_list,
"country": country,
"memberResults": member_results,
}