diff --git a/app/data/taxes.py b/app/data/taxes.py index b96d5d9..9c7bb9c 100644 --- a/app/data/taxes.py +++ b/app/data/taxes.py @@ -136,7 +136,7 @@ class Taxes(util.FloatRepr): income: float medicare: float social_security: float - portfolio: float = -1 + portfolio: float def __float__(self): return float( @@ -152,19 +152,17 @@ def __float__(self): def calc_taxes( - total_income: Income, controller: JobIncomeController, state: State + total_income: Income, + job_income_controller: JobIncomeController, + state: State, + portfolio_return: float, ) -> Taxes: """Calculates taxes for a given interval - Args: - total_income (Income) - controller (JobIncomeController) - state (State) - Returns: Taxes: Attributes: income, medicare, social_security, portfolio """ - taxable_income = controller.get_taxable_income(state.interval_idx) + taxable_income = job_income_controller.get_taxable_income(state.interval_idx) job_income_tax = _calc_income_taxes(interval_income=taxable_income, state=state) pension_income_tax = 0.8 * _calc_income_taxes( interval_income=total_income.social_security_user @@ -175,7 +173,11 @@ def calc_taxes( return Taxes( income=job_income_tax + pension_income_tax, medicare=-total_income.job_income * MEDICARE_TAX_RATE, - social_security=_social_security_tax(controller, state), + social_security=_social_security_tax(job_income_controller, state), + # Assumes user tax-loss harvestes, so tax is always has opposite sign of return + # There is technically an annual limit to harvesting, but losses carry over, + # so for simplicity we assume it is always harvested the year it is incurred. + portfolio=-portfolio_return * state.user.portfolio.tax_rate, ) diff --git a/app/models/config.py b/app/models/config.py index 1cf0911..1176430 100644 --- a/app/models/config.py +++ b/app/models/config.py @@ -216,7 +216,7 @@ class Portfolio(BaseModel): Attributes current_net_worth (float): Defaults to 0 - drawdown_tax_rate (float): Defaults to 0.1 + tax_rate (float): Defaults to 0.1 annuity (AnnuityConfig): Defaults to None @@ -224,7 +224,7 @@ class Portfolio(BaseModel): """ current_net_worth: float = 0 - drawdown_tax_rate: float = 0.1 + tax_rate: float = 0.1 annuity: Optional[AnnuityConfig] = None allocation_strategy: AllocationOptions diff --git a/app/models/financial/state_change.py b/app/models/financial/state_change.py index 5c01a81..9a00f6c 100644 --- a/app/models/financial/state_change.py +++ b/app/models/financial/state_change.py @@ -129,23 +129,22 @@ def _gen_net_transactions(self) -> _NetTransactions: portfolio_return = self._state.net_worth * np.dot( self._economic_data.asset_rates, self._allocation ) - costs = self._gen_costs(income) - annuity = self._controllers.annuity.make_annuity_transaction( - state=self._state, - is_working=self._controllers.job_income.is_working( - self._state.interval_idx - ), - initial_net_transaction=income.job_income + costs, - ) + costs = self._gen_costs(income, portfolio_return) return _NetTransactions( income=income, portfolio_return=portfolio_return, costs=costs, - annuity=annuity, + annuity=self._controllers.annuity.make_annuity_transaction( + state=self._state, + is_working=self._controllers.job_income.is_working( + self._state.interval_idx + ), + initial_net_transaction=income.job_income + costs, + ), ) - def _gen_costs(self, income: Income) -> _Costs: + def _gen_costs(self, income: Income, portfolio_return: float) -> _Costs: spending = _calc_spending( state=self._state, config=self._state.user.spending, @@ -162,7 +161,8 @@ def _gen_costs(self, income: Income) -> _Costs: ), taxes=calc_taxes( total_income=income, - controller=self._controllers.job_income, + job_income_controller=self._controllers.job_income, state=self._state, + portfolio_return=portfolio_return, ), ) diff --git a/app/models/simulator.py b/app/models/simulator.py index 9d3209d..00c422a 100644 --- a/app/models/simulator.py +++ b/app/models/simulator.py @@ -90,16 +90,6 @@ def as_dataframes(self) -> list[pd.DataFrame]: """ Returns a list of pandas DataFrames, where each DataFrame represents a trial in the simulator. - Each DataFrame contains the following columns: - - Date: The date of the interval. - - Net Worth: The net worth of the interval. - - Inflation: The inflation rate of the interval. - - Job Income: The job income of the interval. - - SS User: The social security income of the interval for the user. - - SS Partner: The social security income of the interval for the partner. - - Pension: The pension income of the interval. - - Portfolio Return: The portfolio return of the interval. - - Annuity: The annuity income of the interval. """ columns = [ "Date", @@ -115,6 +105,7 @@ def as_dataframes(self) -> list[pd.DataFrame]: "Income Taxes", "Medicare Taxes", "Social Security Taxes", + "Portfolio Taxes", "Total Taxes", "Total Costs", "Portfolio Return", @@ -138,6 +129,7 @@ def as_dataframes(self) -> list[pd.DataFrame]: interval.state_change_components.net_transactions.costs.taxes.income, interval.state_change_components.net_transactions.costs.taxes.medicare, interval.state_change_components.net_transactions.costs.taxes.social_security, + interval.state_change_components.net_transactions.costs.taxes.portfolio, interval.state_change_components.net_transactions.costs.taxes, interval.state_change_components.net_transactions.costs, interval.state_change_components.net_transactions.portfolio_return, diff --git a/tests/sample_configs/full_config.yml b/tests/sample_configs/full_config.yml index 2ce8ea7..e8c4a34 100644 --- a/tests/sample_configs/full_config.yml +++ b/tests/sample_configs/full_config.yml @@ -8,7 +8,7 @@ net_worth_target: 1500 portfolio: current_net_worth: 250 - drawdown_tax_rate: 0.1 + tax_rate: 0.1 annuity: net_worth_target: 500 contribution_rate: 0.1