![JAR search and dependency download from the Maven repository](/logo.png)
io.fincast.household.impl.ChPillarOne.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of fincast Show documentation
Show all versions of fincast Show documentation
Financial Planning Forecast (Projection)
package io.fincast.household.impl
import io.fincast.enums.FundsAllocation
import io.fincast.enums.Periodicity
import io.fincast.enums.ProductType
import io.fincast.household.Contract
import io.fincast.household.Person
import io.fincast.portfolio.Portfolio
import io.fincast.portfolio.Position
import io.fincast.portfolio.PositionCompo
import io.fincast.portfolio.ValueProviders.constValue
import io.fincast.portfolio.impl.CashflowCompo
import io.fincast.util.SimDate
data class ChPillarOne(
override val tag: String,
override val owner: Person,
val annualPension44: Double? = 0.0,
val missingContributionYears: Int? = 0,
) : Contract, HoldingBase() {
override val productType = ProductType.CONTRACT
override val startDate get() = owner.getActualRetirementDate() + 1
override val endDate get() = getLastDeathDate()
class Builder {
private var tag: String? = null
private var owner: Person? = null
private var annualPension44: Double? = null
private var missingContributionYears: Int? = null
fun tag(tag: String) = apply { this.tag = tag }
fun owner(owner: Person) = apply { this.owner = owner }
fun annualPension44(annualPension44: Double?) = apply { this.annualPension44 = annualPension44 }
fun missingContributionYears(missingContributionYears: Int?) = apply { this.missingContributionYears = missingContributionYears }
fun build(): ChPillarOne {
return ChPillarOne(
tag = tag ?: throw IllegalArgumentException("tag is required"),
owner = owner ?: throw IllegalArgumentException("owner is required"),
annualPension44 = annualPension44 ?: 0.0,
missingContributionYears = missingContributionYears ?: 0,
)
}
}
override fun createCompos(portfolio: Portfolio, pos: Position): List {
return if (isCouple()) {
createCoupleCompos(pos)
} else {
createSingleCompos(pos)
}
}
/**
* There are 3 phases for a swiss first pillar pension of a single person:
* 1. person is working and pays contributions (this is not modeled, since it is accounted for in the net salary)
* 2. person is retired and receives a pension
* 3. person is dead and the pension stops
*/
private fun createSingleCompos(pos: Position): List {
val ownerRetirementDate = owner.getActualRetirementDate()
val ownerDeathDate = owner.deathDate
val ownerHasRetirementPhase = ownerDeathDate == null || ownerDeathDate > ownerRetirementDate
if (ownerHasRetirementPhase) {
val ownerPension = getSinglePension(owner)
return listOf(getPensionCompo(pos, ownerPension, ownerRetirementDate + 1, ownerDeathDate))
}
return emptyList()
}
/**
* There are 9 different combinations of phases for a swiss first pillar pension of a couple:
*
* | owner | partner | pension cashflow from owners pillar one |
* |-------|---------|-------|
* | working | working | n/a (no cashflows) |
* | working | retired | n/a (no cashflows) |
* | working | dead | n/a (but might receive widow pension from partner pillar, if owner is eligible) |
* | retired | working | single pension |
* | retired | retired | couple pension (with pro rata couple cap) |
* | retired | dead | owner single pension, only if higher than partner widow pension |
* | dead | working | widow pension, if partner is eligible |
* | dead | retired | widow pension, only if higher than partners pension |
* | dead | dead | n/a |
*/
private fun createCoupleCompos(pos: Position): List {
val compos: MutableList = mutableListOf()
val ownerRetirementDate = owner.getActualRetirementDate()
val ownerDeathDate = owner.deathDate
val ownerHasRetirementPhase = ownerDeathDate == null || ownerDeathDate > ownerRetirementDate
val ownerBasePension = getBasePension(owner)
val ownerSinglePension = getSinglePension(owner)
//println("\n${pos.holding.tag}.createCoupleCompos.owner($ownerRetirementDate, $ownerDeathDate, $ownerHasRetirementPhase, $ownerBasePension, $ownerSinglePension))")
val partner = getPartner()
val partnerRetirementDate = partner.getActualRetirementDate()
val partnerDeathDate = partner.deathDate
val partnerEndOfWorkDate = SimDate.min(partnerRetirementDate, partnerDeathDate ?: partnerRetirementDate)
val partnerHasRetirementPhase = partnerDeathDate == null || partnerDeathDate > partnerRetirementDate
val partnerBasePension = getBasePension(partner)
val partnerSinglePension = getSinglePension(partner)
//println("${pos.holding.tag}.createCoupleCompos.partner($partnerRetirementDate, $partnerDeathDate, $partnerEndOfWorkDate, $partnerHasRetirementPhase, $partnerBasePension, $partnerSinglePension))\n")
val firstDeathDate = if (ownerDeathDate == null) partnerDeathDate else if (partnerDeathDate == null) null else SimDate.min(ownerDeathDate, partnerDeathDate)
// owner retirement phase (ownerRetirementDate+1 .. ownerDeathDate)
if (ownerHasRetirementPhase) {
// partner working [ownerRetirementDate+1 .. min(ownerDeathDate, min(partnerRetirementDate, partnerDeathDate)]
if (partnerEndOfWorkDate > ownerRetirementDate) {
val workEndDate = if (ownerDeathDate == null) partnerEndOfWorkDate else SimDate.min(ownerDeathDate, partnerEndOfWorkDate)
//println("${pos.holding.tag}.createCoupleCompos.retired/working (${ownerRetirementDate + 1} .. ${workEndDate})")
compos.add(getPensionCompo(pos, ownerSinglePension, ownerRetirementDate + 1, workEndDate))
}
// partner retired [partnerRetirementDate+1 .. min(ownerDeathDate, partnerDeathDate)]
if (firstDeathDate == null || (firstDeathDate > partnerRetirementDate && firstDeathDate > ownerRetirementDate)) {
val retirementStartDate = SimDate.max(partnerRetirementDate, ownerRetirementDate) + 1
var pension = ownerBasePension
val couplePension = pension + partnerBasePension
if (couplePension > MaxCouplePension) {
pension *= MaxCouplePension / couplePension
}
//println("${pos.holding.tag}.createCoupleCompos.retired/retired (${retirementStartDate} .. ${firstDeathDate})")
compos.add(getPensionCompo(pos, pension, retirementStartDate, firstDeathDate))
}
// partner dead (partnerDeathDate+1 .. ownerDeathDate)
if (partnerDeathDate != null && (ownerDeathDate == null || partnerDeathDate + 1 < ownerDeathDate)) {
if (ownerSinglePension >= getWidowPension(partner)) {
val deathStartDate = SimDate.max(partnerDeathDate, ownerRetirementDate) + 1
//println("${pos.holding.tag}.createCoupleCompos.retired/dead (${deathStartDate} .. ${ownerDeathDate})")
compos.add(getPensionCompo(pos, ownerSinglePension, deathStartDate, ownerDeathDate))
}
}
}
// owner death phase
if (ownerDeathDate != null && (partnerDeathDate == null || ownerDeathDate + 1 < partnerDeathDate)) {
val widowPension = getWidowPension()
// partner working (ownerDeathDate+1 .. partnerEndOfWorkDate)
if (ownerDeathDate + 1 < partnerEndOfWorkDate) { // TODO: check eligibility
//println("${pos.holding.tag}.createCoupleCompos.dead/working (${ownerDeathDate + 1} .. ${partnerEndOfWorkDate})")
compos.add(getPensionCompo(pos, widowPension, ownerDeathDate + 1, partnerEndOfWorkDate))
}
// partner retired (partnerEndOfWorkDate+1 .. partnerDeathDate)
if (partnerHasRetirementPhase) {
if (widowPension > partnerSinglePension) {
//println("${pos.holding.tag}.createCoupleCompos.dead/retired (${partnerEndOfWorkDate + 1} .. ${partnerDeathDate})")
compos.add(getPensionCompo(pos, widowPension, partnerEndOfWorkDate + 1, partnerDeathDate))
}
}
}
return compos
}
private fun getPensionCompo(pos: Position, pension: Double, startDate: SimDate?, endDate: SimDate?): PositionCompo {
val monthlyPension = pension / SimDate.MONTHS_PER_YEAR
return CashflowCompo(
position = pos,
tag = "pension",
fundsAllocation = FundsAllocation.DISBURSE,
amount = constValue(monthlyPension),
sign = 1,
startDate = startDate,
endDate = endDate,
periodicity = Periodicity.MONTHLY,
)
}
private fun getBasePension(person: Person): Double {
val pillarOne = getPillarOne(person) ?: return 0.0
return pillarOne.getBasePension()
}
private fun getSinglePension(person: Person): Double {
val pillarOne = getPillarOne(person) ?: return 0.0
return pillarOne.getSinglePension()
}
private fun getWidowPension(person: Person): Double {
val pillarOne = getPillarOne(person) ?: return 0.0
return pillarOne.getWidowPension()
}
private fun getPillarOne(person: Person): ChPillarOne? {
return household.holdings.filterIsInstance().firstOrNull { it.owner == person }
}
private fun getSinglePension(): Double {
var pension = getBasePension()
if (pension > MaxSinglePension) {
pension = MaxSinglePension
}
return pension
}
private fun getWidowPension(): Double {
var pension = 0.8 * getBasePension()
if (pension > MaxSinglePension) {
pension = MaxSinglePension
}
return pension
}
private fun getBasePension(): Double {
val annualPension = this.annualPension44 ?: return 0.0
var pension = annualPension
val retirementAge = owner.getActualRetirementAge()
val stdRetirementAge = owner.getStandardRetirementAge()
if (retirementAge < stdRetirementAge) {
val ageDiff = stdRetirementAge - retirementAge
pension *= (1 - ageDiff * ReductionPerAdvanceYear / 100)
} else if (retirementAge > stdRetirementAge) {
val ageDiff = retirementAge - stdRetirementAge
pension *= (1 + ageDiff * IncreasePerDelayedYear / 100)
}
if (null != missingContributionYears) {
pension *= (1 - missingContributionYears * ReductionPerMissingYear / 100)
}
return pension
}
private fun getPartner(): Person {
require(isCouple()) { "only valid for couples" }
return if (owner == household.partner1) household.partner2!! else household.partner1
}
private fun getLastDeathDate(): SimDate? {
val deathDate = owner.deathDate ?: return null
val partnerDeathDate = household.partner2?.deathDate ?: return deathDate
return SimDate.max(deathDate, partnerDeathDate)
}
private fun isCouple(): Boolean {
return household.partner2 != null
}
companion object {
const val MaxSinglePension = 12.0 * 2450
const val MaxCouplePension = 1.5 * MaxSinglePension
const val ReductionPerMissingYear = 2.27
const val ReductionPerAdvanceYear = 6.8
const val IncreasePerDelayedYear = 5.2
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy