from typing import Any
from typing import List
from typing import Optional
from typing import TYPE_CHECKING
from finalynx.portfolio.bucket import Bucket
from finalynx.portfolio.folder import Folder
from finalynx.portfolio.folder import Portfolio
from finalynx.portfolio.folder import SharedFolder
from finalynx.portfolio.line import Line
from finalynx.portfolio.node import Node
from finalynx.portfolio.targets import TargetRatio
if TYPE_CHECKING:
from finalynx.simulator.events import Event
[docs]class Action:
"""Abstract base class to perform an action on the portfolio."""
def __init__(self, name: Optional[str] = None) -> None:
"""Abstract class. An action describes a procedure to change something in the portfolio.
For instance, when receiving a salary, an action could add some amount to the main account.
"""
self.name = name if name else self.__class__.__name__
[docs] def apply(self, portfolio: Portfolio) -> List["Event"]:
"""Apply this action's consequence, must be overridden."""
raise NotImplementedError("Must be overridden.")
[docs] def __str__(self) -> str:
return self.name
[docs]class SetLineAmount(Action):
"""Set an amount to a line."""
def __init__(self, target_line: Line, amount: float) -> None:
"""This action simply applies the new amount to the line. The timeline then processes
the portfolio again to recalculate the SharedFolders' values.
"""
self.target_line = target_line
self.amount = amount
super().__init__()
[docs] def apply(self, portfolio: Portfolio) -> List["Event"]:
self.target_line.amount = self.amount
return []
[docs]class AddLineAmount(Action):
"""Add some amount to a line."""
def __init__(self, target_line: Line, amount: float) -> None:
"""This action simply applies the new amount to the line. The timeline then processes
the portfolio again to recalculate the SharedFolders' values.
"""
self.target_line = target_line
self.amount = amount
super().__init__()
[docs] def apply(self, portfolio: Portfolio) -> List["Event"]:
self.target_line.amount += self.amount
return []
[docs]class AutoBalance(Action):
"""Automatically apply Finalynx's recommendations on the portfolio."""
[docs] def apply(self, portfolio: Portfolio) -> List["Event"]:
"""This action automatically applies the ideal amounts auto-calculated
in the portfolio tree. This only applies to `Line` and `SharedFolder`
instances that have a `TargetRatio` target. The amounts are balanced
depending on the target percentages for each node.
Lines auto-added by envelope in folders are also balanced with equal
percentages set for each child in the same folder.
"""
ideals = self._get_ideals(portfolio)
self._set_ideals(portfolio, ideals)
return []
[docs] def _get_ideals(self, node: Node) -> List[Any]:
"""Save the ideal amounts calculated in the tree before applying them to
avoid inconsistent states."""
if isinstance(node, Folder) and not isinstance(node, SharedFolder):
return [self._get_ideals(c) for c in node.children]
else:
if (
node.target.__class__.__name__ == "Target"
and node.parent
and isinstance(node.parent.target, TargetRatio)
):
return [node.parent.get_ideal() / len(node.parent.children)]
return [node.get_ideal()]
[docs] def _set_ideals(self, node: Node, ideals: List[Any]) -> None:
"""Set the ideal amounts for each `Line` and `SharedFolder`."""
# Traverse the tree to get to the leaves
if isinstance(node, Folder) and not isinstance(node, SharedFolder):
for i_child, child in enumerate(node.children):
self._set_ideals(child, ideals[i_child])
# At a leaf level, only update the amount if it's a node with a ratio target.
# Add an exception for Lines auto-added in folders (no target set but the parent folder has a ratio)
elif isinstance(node.target, TargetRatio) or (
node.target.__class__.__name__ == "Target" and node.parent and isinstance(node.parent.target, TargetRatio)
):
if isinstance(node, SharedFolder):
node.bucket.add_amount(ideals[0] - node.get_amount())
elif isinstance(node, Line):
node.amount = ideals[0]