mirror of
https://github.com/mblanke/Lottery-Tracker.git
synced 2026-03-01 14:10:22 -05:00
Version 1.1
This commit is contained in:
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user