mirror of
https://github.com/mblanke/Lottery-Tracker.git
synced 2026-03-01 06:00:21 -05:00
345 lines
11 KiB
Python
345 lines
11 KiB
Python
"""
|
|
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.
|
|
"""
|
|
|
|
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)
|
|
"""
|
|
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: 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.
|
|
"""
|
|
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
|
|
|
|
canadian_amount = net_amount * usd_cad_rate
|
|
|
|
investment_principal = canadian_amount * invest_percentage
|
|
fun_money = canadian_amount * (1 - invest_percentage)
|
|
|
|
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",
|
|
"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: 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: 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.
|
|
"""
|
|
net_amount = jackpot # Tax-free!
|
|
|
|
investment_principal = net_amount * invest_percentage
|
|
fun_money = net_amount * (1 - invest_percentage)
|
|
|
|
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",
|
|
"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,
|
|
}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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,
|
|
}
|