From 7b9f2a29deaf4bdbda9bb6161476809c6480d80a Mon Sep 17 00:00:00 2001 From: gcoue Date: Mon, 6 Nov 2023 23:15:06 +0100 Subject: [PATCH 01/17] Change credit card to negative amount --- finalynx/fetch/source_finary.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/finalynx/fetch/source_finary.py b/finalynx/fetch/source_finary.py index 4a441c5..accf602 100644 --- a/finalynx/fetch/source_finary.py +++ b/finalynx/fetch/source_finary.py @@ -122,13 +122,7 @@ def _authenticate(self) -> Optional[Session]: # Login to Finary with the existing cookies file or credentials in environment variables and retrieve data if os.environ.get("FINARY_EMAIL") and os.environ.get("FINARY_PASSWORD"): - self._log("Signing in to Finary...") - with console.status( - f"""[bold {TH().ACCENT}]Signing in to Finary...[/] """ - """[dim white](Type your 2FA code if prompted and press [italic]Enter[/], """ - """it will remain invisible while you type)""", - spinner_style=TH().ACCENT, - ): + with console.status(f"[bold {TH().ACCENT}]Signing in to Finary...", spinner_style=TH().ACCENT): result = ff.signin() self._log("Signed in to Finary.") @@ -182,12 +176,17 @@ def _process_account(self, dict_account: Dict[str, Any], tree: Tree) -> None: node = tree.add(account_name) for item in dict_account["fiats"]: + subtype=dict_account["bank_account_type"]["subtype"] + if subtype == "credit": + amount = -item["display_current_value"] + else: + amount = item["display_current_value"] self._register_fetchline( tree_node=node, name=account_name, id=item["id"], account=dict_account["institution"]["name"], - amount=item["display_current_value"], + amount=amount, currency=item["fiat"]["symbol"], ) From 1983b81993f11f71051418bf5d04c4001fdd9e4d Mon Sep 17 00:00:00 2001 From: gcoue Date: Mon, 6 Nov 2023 23:23:36 +0100 Subject: [PATCH 02/17] Rollback source_finary.py --- finalynx/fetch/source_finary.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/finalynx/fetch/source_finary.py b/finalynx/fetch/source_finary.py index accf602..4a441c5 100644 --- a/finalynx/fetch/source_finary.py +++ b/finalynx/fetch/source_finary.py @@ -122,7 +122,13 @@ def _authenticate(self) -> Optional[Session]: # Login to Finary with the existing cookies file or credentials in environment variables and retrieve data if os.environ.get("FINARY_EMAIL") and os.environ.get("FINARY_PASSWORD"): - with console.status(f"[bold {TH().ACCENT}]Signing in to Finary...", spinner_style=TH().ACCENT): + self._log("Signing in to Finary...") + with console.status( + f"""[bold {TH().ACCENT}]Signing in to Finary...[/] """ + """[dim white](Type your 2FA code if prompted and press [italic]Enter[/], """ + """it will remain invisible while you type)""", + spinner_style=TH().ACCENT, + ): result = ff.signin() self._log("Signed in to Finary.") @@ -176,17 +182,12 @@ def _process_account(self, dict_account: Dict[str, Any], tree: Tree) -> None: node = tree.add(account_name) for item in dict_account["fiats"]: - subtype=dict_account["bank_account_type"]["subtype"] - if subtype == "credit": - amount = -item["display_current_value"] - else: - amount = item["display_current_value"] self._register_fetchline( tree_node=node, name=account_name, id=item["id"], account=dict_account["institution"]["name"], - amount=amount, + amount=item["display_current_value"], currency=item["fiat"]["symbol"], ) From f97a7c7c0b801c821deaf9c7cf1b120ac791fd25 Mon Sep 17 00:00:00 2001 From: gcoue Date: Mon, 6 Nov 2023 23:27:03 +0100 Subject: [PATCH 03/17] Change credit card to negative amount --- finalynx/fetch/source_finary.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/finalynx/fetch/source_finary.py b/finalynx/fetch/source_finary.py index 4a441c5..a4e36ca 100644 --- a/finalynx/fetch/source_finary.py +++ b/finalynx/fetch/source_finary.py @@ -182,12 +182,17 @@ def _process_account(self, dict_account: Dict[str, Any], tree: Tree) -> None: node = tree.add(account_name) for item in dict_account["fiats"]: + subtype=dict_account["bank_account_type"]["subtype"] + if subtype == "credit": + amount = -item["display_current_value"] + else: + amount = item["display_current_value"] self._register_fetchline( tree_node=node, name=account_name, id=item["id"], account=dict_account["institution"]["name"], - amount=item["display_current_value"], + amount=amount, currency=item["fiat"]["symbol"], ) From 721e36af9132ff7e03b3dd66d729e679224ba2af Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Tue, 7 Nov 2023 21:04:59 +0000 Subject: [PATCH 04/17] [pre-commit.ci lite] apply automatic fixes --- finalynx/fetch/source_finary.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/finalynx/fetch/source_finary.py b/finalynx/fetch/source_finary.py index a4e36ca..62e5e1c 100644 --- a/finalynx/fetch/source_finary.py +++ b/finalynx/fetch/source_finary.py @@ -182,7 +182,7 @@ def _process_account(self, dict_account: Dict[str, Any], tree: Tree) -> None: node = tree.add(account_name) for item in dict_account["fiats"]: - subtype=dict_account["bank_account_type"]["subtype"] + subtype = dict_account["bank_account_type"]["subtype"] if subtype == "credit": amount = -item["display_current_value"] else: From a079da793d1bea195fbe31cac22f07e930a45dc3 Mon Sep 17 00:00:00 2001 From: gcoue Date: Wed, 8 Nov 2023 10:29:30 +0100 Subject: [PATCH 05/17] Add a flag to allow the potfolio restitution for each simulation step --- finalynx/assistant.py | 17 +++++++++++++++++ finalynx/simulator/timeline.py | 3 +++ finalynx/usage.py | 1 + 3 files changed, 21 insertions(+) diff --git a/finalynx/assistant.py b/finalynx/assistant.py index 6b6c0cd..09c0fd0 100644 --- a/finalynx/assistant.py +++ b/finalynx/assistant.py @@ -93,6 +93,9 @@ def __init__( self.buckets = buckets if buckets else [] self.envelopes = envelopes if envelopes else [] + #Storage for value of portfolio at each intermediate step + self.intermediate_value = [] + # Options that can either be set in the constructor or from the command line options, see --help self.ignore_orphans = ignore_orphans self.clear_cache = clear_cache @@ -190,6 +193,8 @@ def _parse_args(self) -> None: self.active_sources = str(args["--sources"]).split(",") if args["--future"] and self.simulation: self.simulation.print_final = True + if args["--each-step"] and self.simulation: + self.simulation.print_each_step = True if args["--sim-steps"] and self.simulation: self.simulation.step_years = int(args["--sim-steps"]) if args["--theme"]: @@ -234,6 +239,11 @@ def run(self) -> None: # Add the simulation summary to the performance panel in the console dict_panels["performance"].add(self.simulate()) + # If enabled by the user, print the each_step portfolio during the simulation + if self.simulation.print_each_step: + for element in self.intermediate_value: + renders.append(element) + # If enabled by the user, print the final portfolio after the simulation if self.simulation.print_final: renders.append(f"\nYour portfolio in {self.simulation.end_date}:") @@ -301,6 +311,13 @@ def append_worth(year: int, amount: float) -> None: if (year - date.today().year) % self.simulation.step_years == 0: append_worth(year, self.portfolio.get_amount()) + if self.simulation.print_each_step: + #Storage for each intermediate step of simu + #f"[{TH().TEXT}]Current: [bold][{TH().ACCENT}]{perf:.2f} %[/] / year" + #title = "Your portfolio in "+str(year)+"-12-31:" + title = "Your portfolio in [bold]"+str(year)+"-12-31:[/]" + self.intermediate_value.append(Panel(self.render_mainframe(), title=title)) + # Run until the end date and append the final result self._timeline.run() append_worth(self._timeline.current_date.year, self.portfolio.get_amount()) diff --git a/finalynx/simulator/timeline.py b/finalynx/simulator/timeline.py index 9ffe991..c452da2 100644 --- a/finalynx/simulator/timeline.py +++ b/finalynx/simulator/timeline.py @@ -35,6 +35,9 @@ class Simulation: # Whether to print the final portfolio state in the console after the simulation print_final: bool = False + # Whether to print the final portfolio state in the console after the simulation + print_each_step: bool = False + # Display the portfolio's worth in the console every `step` years step_years: int = 5 diff --git a/finalynx/usage.py b/finalynx/usage.py index f431846..fb393be 100644 --- a/finalynx/usage.py +++ b/finalynx/usage.py @@ -56,5 +56,6 @@ def main_filter(message: str) -> str: --sim-steps=int Display the simulated portfolio's worth every X years, defaults to 5 --future Print the portfolio after the simulation has finished + --each-step Print the portfolio for each step of the simulation """ From 2594dd2310e783cf3121d1f3d985113bd75aedb1 Mon Sep 17 00:00:00 2001 From: gcoue Date: Wed, 8 Nov 2023 10:29:30 +0100 Subject: [PATCH 06/17] Add a flag to allow the potfolio restitution for each simulation step --- finalynx/assistant.py | 14 ++++++++++++++ finalynx/simulator/timeline.py | 3 +++ finalynx/usage.py | 1 + 3 files changed, 18 insertions(+) diff --git a/finalynx/assistant.py b/finalynx/assistant.py index 6b6c0cd..ffd62a1 100644 --- a/finalynx/assistant.py +++ b/finalynx/assistant.py @@ -93,6 +93,9 @@ def __init__( self.buckets = buckets if buckets else [] self.envelopes = envelopes if envelopes else [] + #Storage for value of portfolio at each intermediate step + self.intermediate_value = [] + # Options that can either be set in the constructor or from the command line options, see --help self.ignore_orphans = ignore_orphans self.clear_cache = clear_cache @@ -190,6 +193,8 @@ def _parse_args(self) -> None: self.active_sources = str(args["--sources"]).split(",") if args["--future"] and self.simulation: self.simulation.print_final = True + if args["--each-step"] and self.simulation: + self.simulation.print_each_step = True if args["--sim-steps"] and self.simulation: self.simulation.step_years = int(args["--sim-steps"]) if args["--theme"]: @@ -234,6 +239,11 @@ def run(self) -> None: # Add the simulation summary to the performance panel in the console dict_panels["performance"].add(self.simulate()) + # If enabled by the user, print the each_step portfolio during the simulation + if self.simulation.print_each_step: + for element in self.intermediate_value: + renders.append(element) + # If enabled by the user, print the final portfolio after the simulation if self.simulation.print_final: renders.append(f"\nYour portfolio in {self.simulation.end_date}:") @@ -300,6 +310,10 @@ def append_worth(year: int, amount: float) -> None: if (year - date.today().year) % self.simulation.step_years == 0: append_worth(year, self.portfolio.get_amount()) + if self.simulation.print_each_step: + #Storage for each intermediate simulation step + title = "Your portfolio in [bold]"+str(year)+"-12-31:[/]" + self.intermediate_value.append(Panel(self.render_mainframe(), title=title)) # Run until the end date and append the final result self._timeline.run() diff --git a/finalynx/simulator/timeline.py b/finalynx/simulator/timeline.py index 9ffe991..c452da2 100644 --- a/finalynx/simulator/timeline.py +++ b/finalynx/simulator/timeline.py @@ -35,6 +35,9 @@ class Simulation: # Whether to print the final portfolio state in the console after the simulation print_final: bool = False + # Whether to print the final portfolio state in the console after the simulation + print_each_step: bool = False + # Display the portfolio's worth in the console every `step` years step_years: int = 5 diff --git a/finalynx/usage.py b/finalynx/usage.py index f431846..fb393be 100644 --- a/finalynx/usage.py +++ b/finalynx/usage.py @@ -56,5 +56,6 @@ def main_filter(message: str) -> str: --sim-steps=int Display the simulated portfolio's worth every X years, defaults to 5 --future Print the portfolio after the simulation has finished + --each-step Print the portfolio for each step of the simulation """ From fc196ddd4010dc34fb77c2fc6529ccd800b9757c Mon Sep 17 00:00:00 2001 From: gcoue Date: Wed, 3 Jan 2024 22:37:53 +0100 Subject: [PATCH 07/17] feat(dashboard): add portfolio simulation evolution --- finalynx/analyzer/asset_class.py | 26 +++++ finalynx/analyzer/envelopes.py | 34 ++++++ finalynx/analyzer/lines.py | 47 ++++++++ finalynx/analyzer/subasset_class.py | 165 ++++++++++++++++++++++++++++ finalynx/assistant.py | 2 + finalynx/dashboard/dashboard.py | 19 +++- finalynx/portfolio/constants.py | 1 + finalynx/simulator/timeline.py | 135 ++++++++++++++++++----- finalynx/usage.py | 1 + 9 files changed, 403 insertions(+), 27 deletions(-) create mode 100644 finalynx/analyzer/lines.py create mode 100644 finalynx/analyzer/subasset_class.py diff --git a/finalynx/analyzer/asset_class.py b/finalynx/analyzer/asset_class.py index f15062d..6e287b6 100644 --- a/finalynx/analyzer/asset_class.py +++ b/finalynx/analyzer/asset_class.py @@ -1,3 +1,4 @@ +from datetime import date from typing import Any from typing import Dict @@ -72,6 +73,31 @@ def analyze(self) -> Dict[str, Any]: sum of investments corresponding to each class.""" return self._recursive_merge(self.node) + def analyzeTime(self, target_date: date) -> Dict[str, float]: + """:returns: A dictionary with keys as the asset class names and values as the + sum of investments corresponding to each class.""" + return self._recursive_mergeTime(self.node, target_date) + + def _recursive_mergeTime(self, node: Node, target_date: date) -> Dict[str, Any]: + """Internal method for recursive searching.""" + total = {c.value: 0.0 for c in AssetClass} + + # Lines simply return their own amount + if isinstance(node, Line): + total[node.asset_class.value] = node.get_amount() + return total + + # Folders merge what the children return + elif isinstance(node, Folder): + for child in node.children: + for key, value in self._recursive_mergeTime(child, target_date).items(): + total[key] += value + return total + + # Safeguard for future versions + else: + raise ValueError(f"Unknown node type '{type(node)}'.") + def _recursive_merge(self, node: Node) -> Dict[str, Any]: """Internal method for recursive searching.""" result: Dict[str, Any] = { diff --git a/finalynx/analyzer/envelopes.py b/finalynx/analyzer/envelopes.py index 7486b17..988ea00 100644 --- a/finalynx/analyzer/envelopes.py +++ b/finalynx/analyzer/envelopes.py @@ -1,6 +1,9 @@ +from datetime import date from typing import Any from typing import Dict +from finalynx.portfolio.constants import EnvelopeClass + from ..portfolio import Folder from ..portfolio import Line from ..portfolio import Node @@ -19,6 +22,37 @@ def analyze(self) -> Dict[str, float]: sum of investments corresponding to each class.""" return self._recursive_merge(self.node) + def analyzeTime(self, target_date: date) -> Dict[str, float]: + """:returns: A dictionary with keys as the asset class names and values as the + sum of investments corresponding to each class.""" + return self._recursive_mergeTime(self.node, target_date) + + def _recursive_mergeTime(self, node: Node, target_date: date) -> Dict[str, Any]: + """Internal method for recursive searching.""" + total = {} + + # Lines simply return their own amount + if isinstance(node, Line): + if node.envelope: + total[node.envelope.name] = node.get_amount() + else: + total["Unknown"] = node.get_amount() + return total + + # Folders merge what the children return + elif isinstance(node, Folder): + for child in node.children: + for key, value in self._recursive_mergeTime(child,target_date).items(): + if key in total.keys(): + total[key] += value + else: + total[key] = value + return total + + # Safeguard for future versions + else: + raise ValueError(f"Unknown node type '{type(node)}'.") + def _recursive_merge(self, node: Node) -> Dict[str, float]: """Internal method for recursive searching.""" total = {} diff --git a/finalynx/analyzer/lines.py b/finalynx/analyzer/lines.py new file mode 100644 index 0000000..7449f5a --- /dev/null +++ b/finalynx/analyzer/lines.py @@ -0,0 +1,47 @@ +from datetime import date +from typing import Any +from typing import Dict + +from ..portfolio import Folder +from ..portfolio import Line +from ..portfolio import Node +from .analyzer import Analyzer + + +class AnalyzeLines(Analyzer): + """Aims to agglomerate the children's pf lines and return + the amount represented by each line. + :returns: a dictionary with lines as keys and the + corresponding total amount contained in the children. + """ + + def analyzeTime(self, target_date: date) -> Dict[str, float]: + """:returns: A dictionary with keys as the asset class names and values as the + sum of investments corresponding to each class.""" + return self._recursive_mergeTime(self.node, target_date) + + def _recursive_mergeTime(self, node: Node, target_date: date) -> Dict[str, Any]: + """Internal method for recursive searching.""" + total = {} + + # Lines simply return their own amount + if isinstance(node, Line): + if node.name: + total[node.name] = node.get_amount() + else: + total["Unknown"] = node.get_amount() + return total + + # Folders merge what the children return + elif isinstance(node, Folder): + for child in node.children: + for key, value in self._recursive_mergeTime(child,target_date).items(): + if key in total.keys(): + total[key] += value + else: + total[key] = value + return total + + # Safeguard for future versions + else: + raise ValueError(f"Unknown node type '{type(node)}'.") diff --git a/finalynx/analyzer/subasset_class.py b/finalynx/analyzer/subasset_class.py new file mode 100644 index 0000000..daf67bf --- /dev/null +++ b/finalynx/analyzer/subasset_class.py @@ -0,0 +1,165 @@ +from datetime import date +from typing import Any +from typing import Dict + +import numpy as np + +from ..portfolio import AssetClass +from ..portfolio import AssetSubclass +from ..portfolio import Folder +from ..portfolio import Line +from ..portfolio import Node +from .analyzer import Analyzer + + +class AnalyzeSubAssetClasses(Analyzer): + """Aims to agglomerate the children's Sub asset classes and return + the amount represented by each Sub asset class. + :returns: a dictionary with Sub asset classes as keys and the + corresponding total amount contained in the children. + """ + + SUBASSET_COLORS_FINARY = { + # Cash + "Comptes courants": "#eed7b4", + "Monétaire": "#eed7b4", + "Liquidités": "#eed7b4", + # Guaranteed investments (mostly french) + "Livrets": "#b966f5", + "Livrets imposables": "#b966f5", + "Fonds euro": "#b966f5", + # Bonds + "Fonds datés": "#87bc45", + # Stocks + "Titres vifs": "#3a84de", + "ETF": "#3a84de", + # Real estate + "Immobilier physique": "#deab5e", + "SCPI": "#deab5e", + "SCI": "#deab5e", + # Metals + "Or": "#77cfac", + "Argent": "#77cfac", + "Matières premières": "#77cfac", + # Cryptos + "L1": "#bdcf32", + "Stablecoins": "#bdcf32", + "DeFi": "#bdcf32", + # Passives + "Véhicule": "#434348", + "Passif": "#434348", + # Exotics + "Forêts": "#228c83", + "Art": "#228c83", + "Watches": "#228c83", + "Crowdlending": "#228c83", + "Startup": "#228c83", + # Diversified + "Diversifié": "#b54093", + "OPCVM": "#b54093", + # Unknown (default) + "Unknown": "#b54053", + } + + SUBASSET_COLORS_CUSTOM = { + # Cash + "Comptes courants": "#eed7b4", + "Monétaire": "#eed7b4", + "Liquidités": "#eed7b4", + # Guaranteed investments (mostly french) + "Livrets": "#b966f5", + "Livrets imposables": "#b966f5", + "Fonds euro": "#b966f5", + # Bonds + "Fonds datés": "#87bc45", + # Stocks + "Titres vifs": "#3a84de", + "ETF": "#3a84de", + # Real estate + "Immobilier physique": "#deab5e", + "SCPI": "#deab5e", + "SCI": "#deab5e", + # Metals + "Or": "#77cfac", + "Argent": "#77cfac", + "Matières premières": "#77cfac", + # Cryptos + "L1": "#bdcf32", + "Stablecoins": "#bdcf32", + "DeFi": "#bdcf32", + # Passives + "Véhicule": "#434348", + "Passif": "#434348", + # Exotics + "Forêts": "#228c83", + "Art": "#228c83", + "Watches": "#228c83", + "Crowdlending": "#228c83", + "Startup": "#228c83", + # Diversified + "Diversifié": "#b54093", + "OPCVM": "#b54093", + # Unknown (default) + "Unknown": "#b54053", + } + + def analyze(self) -> Dict[str, Any]: + """:returns: A dictionary with keys as the asset class names and values as the + sum of investments corresponding to each class.""" + return self._recursive_merge(self.node) + + def analyzeTime(self, target_date: date) -> Dict[str, float]: + """:returns: A dictionary with keys as the Sub asset class names and values as the + sum of investments corresponding to each class.""" + return self._recursive_mergeTime(self.node, target_date) + + def _recursive_mergeTime(self, node: Node, target_date: date) -> Dict[str, Any]: + """Internal method for recursive searching.""" + total = {} + + # Lines simply return their own amount + if isinstance(node, Line): + total[node.asset_subclass.value] = node.get_amount() + return total + + # Folders merge what the children return + elif isinstance(node, Folder): + for child in node.children: + for key, value in self._recursive_mergeTime(child, target_date).items(): + if key in total.keys(): + total[key] += value + else: + total[key] = value + # for subkey, subvalue in value["subclasses"].items(): + # total[subkey] += subvalue + return total + + # Safeguard for future versions + else: + raise ValueError(f"Unknown node type '{type(node)}'.") + + def _recursive_merge(self, node: Node) -> Dict[str, Any]: + """Internal method for recursive searching.""" + result: Dict[str, Any] = { + c.value: {"total": 0.0, "subclasses": {s.value: 0.0 for s in AssetSubclass}} for c in AssetClass + } + + # Lines simply return their own amount + if isinstance(node, Line): + result[node.asset_class.value]["total"] = node.get_amount() + result[node.asset_class.value]["subclasses"][node.asset_subclass.value] = node.get_amount() + return result + + # Folders merge what the children return + elif isinstance(node, Folder): + for child in node.children: + for key, subdict in self._recursive_merge(child).items(): + result[key]["total"] += subdict["total"] + + for subkey, subvalue in subdict["subclasses"].items(): + result[key]["subclasses"][subkey] += subvalue + return result + + # Safeguard for future versions + else: + raise ValueError(f"Unknown node type '{type(node)}'.") diff --git a/finalynx/assistant.py b/finalynx/assistant.py index ffd62a1..ff14eb5 100644 --- a/finalynx/assistant.py +++ b/finalynx/assistant.py @@ -197,6 +197,8 @@ def _parse_args(self) -> None: self.simulation.print_each_step = True if args["--sim-steps"] and self.simulation: self.simulation.step_years = int(args["--sim-steps"]) + if args["--metric-frequency"] and self.simulation: + self.simulation.metrics_record_freqency = str(args["--metric-frequency"]) if args["--theme"]: theme_name = str(args["--theme"]) if theme_name not in finalynx.theme.AVAILABLE_THEMES: diff --git a/finalynx/dashboard/dashboard.py b/finalynx/dashboard/dashboard.py index 31e47fb..1968a87 100644 --- a/finalynx/dashboard/dashboard.py +++ b/finalynx/dashboard/dashboard.py @@ -12,6 +12,7 @@ from finalynx.analyzer.asset_class import AnalyzeAssetClasses from finalynx.analyzer.envelopes import AnalyzeEnvelopes from finalynx.analyzer.investment_state import AnalyzeInvestmentStates +from finalynx.analyzer.subasset_class import AnalyzeSubAssetClasses from finalynx.portfolio.folder import Folder from finalynx.portfolio.folder import FolderDisplay from finalynx.portfolio.line import Line @@ -181,7 +182,23 @@ def _on_select_color_map(data: Any) -> None: ) with ui.row(): self.chart_envelopes = ui.chart(AnalyzeEnvelopes(self.selected_node).chart()) - self.chart_simulation = ui.chart(timeline.chart() if timeline else {}) + #self.chart_simulation = ui.chart(timeline.chart() if timeline else {}) + self.chart_etats_enveloppes = ui.chart(timeline.chartOnTimeline("Evolution des états d'enveloppes",timeline._log_env_states,{ + "Unknown": "#434348", + "Closed": "#999999", + "Locked": "#F94144", + "Taxed": "#F9C74F", + "Free": "#7BB151", + }) if timeline else {}) + #self.chart_simulation1 = ui.chart(timeline.chartEnveloppeTimeline() if timeline else {}) + self.chart_enveloppes = ui.chart(timeline.chartOnTimeline("Evolution des enveloppes",timeline._log_enveloppe_values) if timeline else {}) + #self.chart_simulation2 = ui.chart(timeline.OLDchartBucket() if timeline else {}) + #self.chart_simulation3 = ui.chart(timeline.chartAssetTimeline() if timeline else {}) + self.chart_asset_classes = ui.chart(timeline.chartOnTimeline("Evolution des classes d'actifs",timeline._log_assets_classes_values,AnalyzeAssetClasses.ASSET_COLORS_FINARY) if timeline else {}) + self.chart_subasset_classes = ui.chart(timeline.chartOnTimeline("Evolution des sous-classes d'actifs",timeline._log_assets_subclasses_values,AnalyzeSubAssetClasses.SUBASSET_COLORS_FINARY) if timeline else {}) + #self.chart_simulation4 = ui.chart(timeline.chartSubAssetTimeline() if timeline else {}) + self.chart_lines = ui.chart(timeline.chartOnTimeline("Evolution des lignes du portefeuille", timeline._log_lines_values, visible_by_default=False) if timeline else {}) + #self.chart_simulation5 = ui.chart(timeline.chartLinesTimeline() if timeline else {}) ui.run( title="Finalynx Dashboard", diff --git a/finalynx/portfolio/constants.py b/finalynx/portfolio/constants.py index aecc52b..f767794 100644 --- a/finalynx/portfolio/constants.py +++ b/finalynx/portfolio/constants.py @@ -93,6 +93,7 @@ class AssetSubclass(Enum): # Passives VEHICLE = "Véhicule" + PASSIVE = "Passif" # Unknown (default) UNKNOWN = "Unknown" diff --git a/finalynx/simulator/timeline.py b/finalynx/simulator/timeline.py index c452da2..4a43830 100644 --- a/finalynx/simulator/timeline.py +++ b/finalynx/simulator/timeline.py @@ -1,13 +1,18 @@ from dataclasses import dataclass -from datetime import date +from datetime import date, datetime from datetime import timedelta from typing import Any from typing import Dict from typing import List from typing import Optional +from finalynx.analyzer.asset_class import AnalyzeAssetClasses +from finalynx.analyzer.envelopes import AnalyzeEnvelopes +from finalynx.analyzer.lines import AnalyzeLines from finalynx.analyzer.investment_state import AnalyzeInvestmentStates +from finalynx.analyzer.subasset_class import AnalyzeSubAssetClasses from finalynx.portfolio.bucket import Bucket +from finalynx.portfolio.constants import AssetClass from finalynx.portfolio.envelope import EnvelopeState from finalynx.portfolio.folder import Portfolio from finalynx.simulator.actions import AutoBalance @@ -35,12 +40,14 @@ class Simulation: # Whether to print the final portfolio state in the console after the simulation print_final: bool = False - # Whether to print the final portfolio state in the console after the simulation + # Whether to print the portfolio state in the console on each step of the simulation print_each_step: bool = False # Display the portfolio's worth in the console every `step` years step_years: int = 5 + # Record the portfolio stats on each day of the simulation 'DAY', 'MONTH', 'YEAR' + metrics_record_freqency: str = "MONTH" class Timeline: """Main simulation engine to execute programmed actions on your portfolio.""" @@ -74,6 +81,11 @@ def __init__( # Log some metrics during the simulation to display them at the end self._log_dates: List[date] = [] # Dates at which the portfolio metrics were logged self._log_env_states: Dict[str, List[float]] = {c.value: [] for c in EnvelopeState} + self._log_enveloppe_values: Dict[str, List[float]] = {} + self._log_assets_classes_values: Dict[str, List[float]] = {c.value: [] for c in AssetClass} + self._log_assets_subclasses_values: Dict[str, List[float]] = {} + self._log_lines_values: Dict[str, List[float]] = {} + self._log_events: Dict[date, List[str]] = {} def run(self) -> None: """Step all events until the simulation limit is reached.""" @@ -93,6 +105,9 @@ def step_until(self, target_date: date) -> None: """Execute all events until the specified date is reached.""" assert self.current_date < target_date, "Target date must be in the future." + #Enregistrement de la situation de démarrage du Portefeuille + self._record_metrics() + while self.current_date < target_date and not self.is_finished: if self.step(): return @@ -111,6 +126,10 @@ def step(self) -> bool: # Add the newly generated events and sort the event list by date new_events = next_event.apply(self._portfolio) + if next_event.planned_date in self._log_events: + self._log_events[next_event.planned_date].append(next_event.name) + else: + self._log_events[next_event.planned_date]=[next_event.name] # Recalculate the amounts for shared folders for bucket in self._buckets: @@ -123,7 +142,10 @@ def step(self) -> bool: self._sort_events() # Record the metrics if the year changed - if next_event.planned_date.year != self.current_date.year: + #if next_event.planned_date.year != self.current_date.year: + # self._record_metrics() + if (self.simulation.metrics_record_freqency == "DAY" and next_event.planned_date != self.current_date) or (self.simulation.metrics_record_freqency == "YEAR" and next_event.planned_date.year != self.current_date.year) or (self.simulation.metrics_record_freqency == "MONTH" and next_event.planned_date.month != self.current_date.month): + self.current_date = next_event.planned_date self._record_metrics() # Move the current date to this event's date @@ -146,29 +168,65 @@ def is_finished(self) -> bool: def _record_metrics(self) -> None: """Record the portfolio's metrics at the current date to display later.""" - self._log_dates.append(self.current_date) - - # Record the envelope states and their amounts at this date - for key, value in AnalyzeInvestmentStates(self._portfolio).analyze(self.current_date).items(): - self._log_env_states[key].append(value) - - def chart(self) -> Dict[str, Any]: - """Plot a Highcharts chart of the portfolio's envelopes' states and amounts over time.""" - assert self._log_env_states, "Run the simulation before charting." - - colors = { - "Unknown": "#434348", - "Closed": "#999999", - "Locked": "#F94144", - "Taxed": "#F9C74F", - "Free": "#7BB151", - } + if self.current_date not in self._log_dates: + self._log_dates.append(self.current_date) + + # Record the envelope states and their amounts at this date + for key, value in AnalyzeInvestmentStates(self._portfolio).analyze(self.current_date).items(): + self._log_env_states[key].append(value) + + for key, value in AnalyzeEnvelopes(self._portfolio).analyzeTime(self.current_date).items(): + if key in self._log_enveloppe_values: + self._log_enveloppe_values[key].append(value) + else: + self._log_enveloppe_values[key]=[value] + + for key, value in AnalyzeAssetClasses(self._portfolio).analyzeTime(self.current_date).items(): + self._log_assets_classes_values[key].append(value) + + for key, value in AnalyzeSubAssetClasses(self._portfolio).analyzeTime(self.current_date).items(): + if key in self._log_assets_subclasses_values: + self._log_assets_subclasses_values[key].append(value) + else: + self._log_assets_subclasses_values[key]=[value] + + for key, value in AnalyzeLines(self._portfolio).analyzeTime(self.current_date).items(): + if key in self._log_lines_values: + self._log_lines_values[key].append(value) + else: + self._log_lines_values[key]=[value] + else: + #On doit remplacer les valeurs stockées par les nouvelles sans créer 2 fois l'enregistrement + ident = self._log_dates.index(self.current_date) + # Record the envelope states and their amounts at this date + for key, value in AnalyzeInvestmentStates(self._portfolio).analyze(self.current_date).items(): + self._log_env_states[key][-1]=value + for key, value in AnalyzeEnvelopes(self._portfolio).analyzeTime(self.current_date).items(): + if key in self._log_enveloppe_values: + self._log_enveloppe_values[key][-1]=value + else: + self._log_enveloppe_values[key]=[value] + for key, value in AnalyzeAssetClasses(self._portfolio).analyzeTime(self.current_date).items(): + self._log_assets_classes_values[key][-1]=value + for key, value in AnalyzeSubAssetClasses(self._portfolio).analyzeTime(self.current_date).items(): + if key in self._log_assets_subclasses_values: + self._log_assets_subclasses_values[key][-1]=value + else: + self._log_assets_subclasses_values[key]=[value] + for key, value in AnalyzeLines(self._portfolio).analyzeTime(self.current_date).items(): + if key in self._log_lines_values: + self._log_lines_values[key][-1]=value + else: + self._log_lines_values[key]=[value] + + def chartOnTimeline(self, title: str, valuesToGraph: Dict[str, List[float]], colors: Dict[str,str]={}, visible_by_default: bool = True) -> Dict[str, Any]: + """Plot a Highcharts chart of the portfolio's caracteristics and amounts over time.""" + #assert self._log_enveloppe_values, "Run the simulation before charting." return { - "chart": {"plotBackgroundColor": None, "plotBorderWidth": None, "plotShadow": False, "type": "area"}, - "title": {"text": "Simulation", "align": "center"}, + "chart": {"plotBackgroundColor": None, "plotBorderWidth": None, "plotShadow": False, "type": "area", "zooming": {"type": 'xy' }, "height":1200, "width":1000}, + "title": {"text": title, "align": "center"}, "plotOptions": { - "series": {"pointStart": self._log_dates[0].year}, "area": { "stacking": "normal", "lineColor": "#666666", @@ -179,14 +237,39 @@ def chart(self) -> Dict[str, Any]: "series": [ { "name": key, - "data": value, - "color": colors[key], + "data": self.convertDataSeries(value), + "visible": visible_by_default, + "color": colors[key] if (key in colors) else {None} } - for key, value in self._log_env_states.items() + for key, value in valuesToGraph.items() ], + "xAxis": {"type": 'datetime'}, + "yAxis": {"crosshair": True}, + "tooltip": { + "xDateFormat": '%m %Y', + "pointFormat": '{point.x:%e/%m/%Y}: {point.y:,.0f}€
', + "footerFormat": '{series.name}', + }, "credits": {"enabled": False}, } + def convertDataSeries(self, data: [float])->[Any]: + """Convert DataSeries in a time series format to allow non regular data""" + res=[] + i = 0 + while i < len(data): + if (self._log_dates[i] in self._log_events): + evenements = "* "+"
* ".join(self._log_events[self._log_dates[i]]) + else: + evenements = "" + point = {"x": datetime.combine(self._log_dates[i], datetime.min.time()).timestamp() * 1000, + "y": data[i], + "name": evenements + } + res.append(point) + i+=1 + return res + def _sort_events(self) -> None: """Internal method to sort the event list by planned date.""" self._events.sort(key=lambda event: event.planned_date) diff --git a/finalynx/usage.py b/finalynx/usage.py index fb393be..1259a59 100644 --- a/finalynx/usage.py +++ b/finalynx/usage.py @@ -57,5 +57,6 @@ def main_filter(message: str) -> str: --sim-steps=int Display the simulated portfolio's worth every X years, defaults to 5 --future Print the portfolio after the simulation has finished --each-step Print the portfolio for each step of the simulation + --metric-frequency Record the portfolio stats on each day of the simulation 'DAY', 'MONTH', 'YEAR' """ From 84e064da2e9659c389555b485eaead577dfec634 Mon Sep 17 00:00:00 2001 From: gcoue Date: Wed, 3 Jan 2024 23:37:41 +0100 Subject: [PATCH 08/17] Remove comments --- finalynx/dashboard/dashboard.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/finalynx/dashboard/dashboard.py b/finalynx/dashboard/dashboard.py index 1968a87..7cbd9b3 100644 --- a/finalynx/dashboard/dashboard.py +++ b/finalynx/dashboard/dashboard.py @@ -182,7 +182,6 @@ def _on_select_color_map(data: Any) -> None: ) with ui.row(): self.chart_envelopes = ui.chart(AnalyzeEnvelopes(self.selected_node).chart()) - #self.chart_simulation = ui.chart(timeline.chart() if timeline else {}) self.chart_etats_enveloppes = ui.chart(timeline.chartOnTimeline("Evolution des états d'enveloppes",timeline._log_env_states,{ "Unknown": "#434348", "Closed": "#999999", @@ -190,15 +189,10 @@ def _on_select_color_map(data: Any) -> None: "Taxed": "#F9C74F", "Free": "#7BB151", }) if timeline else {}) - #self.chart_simulation1 = ui.chart(timeline.chartEnveloppeTimeline() if timeline else {}) self.chart_enveloppes = ui.chart(timeline.chartOnTimeline("Evolution des enveloppes",timeline._log_enveloppe_values) if timeline else {}) - #self.chart_simulation2 = ui.chart(timeline.OLDchartBucket() if timeline else {}) - #self.chart_simulation3 = ui.chart(timeline.chartAssetTimeline() if timeline else {}) self.chart_asset_classes = ui.chart(timeline.chartOnTimeline("Evolution des classes d'actifs",timeline._log_assets_classes_values,AnalyzeAssetClasses.ASSET_COLORS_FINARY) if timeline else {}) self.chart_subasset_classes = ui.chart(timeline.chartOnTimeline("Evolution des sous-classes d'actifs",timeline._log_assets_subclasses_values,AnalyzeSubAssetClasses.SUBASSET_COLORS_FINARY) if timeline else {}) - #self.chart_simulation4 = ui.chart(timeline.chartSubAssetTimeline() if timeline else {}) self.chart_lines = ui.chart(timeline.chartOnTimeline("Evolution des lignes du portefeuille", timeline._log_lines_values, visible_by_default=False) if timeline else {}) - #self.chart_simulation5 = ui.chart(timeline.chartLinesTimeline() if timeline else {}) ui.run( title="Finalynx Dashboard", From 207b7e9b6d20c690603cd6c425f288ec674a68e1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Thu, 4 Jan 2024 16:59:27 +0000 Subject: [PATCH 09/17] [pre-commit.ci lite] apply automatic fixes --- finalynx/analyzer/asset_class.py | 2 +- finalynx/analyzer/envelopes.py | 4 +- finalynx/analyzer/lines.py | 4 +- finalynx/analyzer/subasset_class.py | 4 +- finalynx/assistant.py | 10 +-- finalynx/dashboard/dashboard.py | 58 ++++++++++++--- finalynx/simulator/timeline.py | 107 +++++++++++++++++----------- finalynx/usage.py | 2 +- 8 files changed, 127 insertions(+), 64 deletions(-) diff --git a/finalynx/analyzer/asset_class.py b/finalynx/analyzer/asset_class.py index 6e287b6..ea3bebe 100644 --- a/finalynx/analyzer/asset_class.py +++ b/finalynx/analyzer/asset_class.py @@ -77,7 +77,7 @@ def analyzeTime(self, target_date: date) -> Dict[str, float]: """:returns: A dictionary with keys as the asset class names and values as the sum of investments corresponding to each class.""" return self._recursive_mergeTime(self.node, target_date) - + def _recursive_mergeTime(self, node: Node, target_date: date) -> Dict[str, Any]: """Internal method for recursive searching.""" total = {c.value: 0.0 for c in AssetClass} diff --git a/finalynx/analyzer/envelopes.py b/finalynx/analyzer/envelopes.py index 988ea00..329bf41 100644 --- a/finalynx/analyzer/envelopes.py +++ b/finalynx/analyzer/envelopes.py @@ -26,7 +26,7 @@ def analyzeTime(self, target_date: date) -> Dict[str, float]: """:returns: A dictionary with keys as the asset class names and values as the sum of investments corresponding to each class.""" return self._recursive_mergeTime(self.node, target_date) - + def _recursive_mergeTime(self, node: Node, target_date: date) -> Dict[str, Any]: """Internal method for recursive searching.""" total = {} @@ -42,7 +42,7 @@ def _recursive_mergeTime(self, node: Node, target_date: date) -> Dict[str, Any]: # Folders merge what the children return elif isinstance(node, Folder): for child in node.children: - for key, value in self._recursive_mergeTime(child,target_date).items(): + for key, value in self._recursive_mergeTime(child, target_date).items(): if key in total.keys(): total[key] += value else: diff --git a/finalynx/analyzer/lines.py b/finalynx/analyzer/lines.py index 7449f5a..f93f540 100644 --- a/finalynx/analyzer/lines.py +++ b/finalynx/analyzer/lines.py @@ -19,7 +19,7 @@ def analyzeTime(self, target_date: date) -> Dict[str, float]: """:returns: A dictionary with keys as the asset class names and values as the sum of investments corresponding to each class.""" return self._recursive_mergeTime(self.node, target_date) - + def _recursive_mergeTime(self, node: Node, target_date: date) -> Dict[str, Any]: """Internal method for recursive searching.""" total = {} @@ -35,7 +35,7 @@ def _recursive_mergeTime(self, node: Node, target_date: date) -> Dict[str, Any]: # Folders merge what the children return elif isinstance(node, Folder): for child in node.children: - for key, value in self._recursive_mergeTime(child,target_date).items(): + for key, value in self._recursive_mergeTime(child, target_date).items(): if key in total.keys(): total[key] += value else: diff --git a/finalynx/analyzer/subasset_class.py b/finalynx/analyzer/subasset_class.py index daf67bf..7bd9fc8 100644 --- a/finalynx/analyzer/subasset_class.py +++ b/finalynx/analyzer/subasset_class.py @@ -47,7 +47,7 @@ class AnalyzeSubAssetClasses(Analyzer): "DeFi": "#bdcf32", # Passives "Véhicule": "#434348", - "Passif": "#434348", + "Passif": "#434348", # Exotics "Forêts": "#228c83", "Art": "#228c83", @@ -112,7 +112,7 @@ def analyzeTime(self, target_date: date) -> Dict[str, float]: """:returns: A dictionary with keys as the Sub asset class names and values as the sum of investments corresponding to each class.""" return self._recursive_mergeTime(self.node, target_date) - + def _recursive_mergeTime(self, node: Node, target_date: date) -> Dict[str, Any]: """Internal method for recursive searching.""" total = {} diff --git a/finalynx/assistant.py b/finalynx/assistant.py index ff14eb5..606cbaf 100644 --- a/finalynx/assistant.py +++ b/finalynx/assistant.py @@ -93,7 +93,7 @@ def __init__( self.buckets = buckets if buckets else [] self.envelopes = envelopes if envelopes else [] - #Storage for value of portfolio at each intermediate step + # Storage for value of portfolio at each intermediate step self.intermediate_value = [] # Options that can either be set in the constructor or from the command line options, see --help @@ -194,11 +194,11 @@ def _parse_args(self) -> None: if args["--future"] and self.simulation: self.simulation.print_final = True if args["--each-step"] and self.simulation: - self.simulation.print_each_step = True + self.simulation.print_each_step = True if args["--sim-steps"] and self.simulation: self.simulation.step_years = int(args["--sim-steps"]) if args["--metric-frequency"] and self.simulation: - self.simulation.metrics_record_freqency = str(args["--metric-frequency"]) + self.simulation.metrics_record_freqency = str(args["--metric-frequency"]) if args["--theme"]: theme_name = str(args["--theme"]) if theme_name not in finalynx.theme.AVAILABLE_THEMES: @@ -313,8 +313,8 @@ def append_worth(year: int, amount: float) -> None: if (year - date.today().year) % self.simulation.step_years == 0: append_worth(year, self.portfolio.get_amount()) if self.simulation.print_each_step: - #Storage for each intermediate simulation step - title = "Your portfolio in [bold]"+str(year)+"-12-31:[/]" + # Storage for each intermediate simulation step + title = "Your portfolio in [bold]" + str(year) + "-12-31:[/]" self.intermediate_value.append(Panel(self.render_mainframe(), title=title)) # Run until the end date and append the final result diff --git a/finalynx/dashboard/dashboard.py b/finalynx/dashboard/dashboard.py index 7cbd9b3..2eabd14 100644 --- a/finalynx/dashboard/dashboard.py +++ b/finalynx/dashboard/dashboard.py @@ -182,17 +182,53 @@ def _on_select_color_map(data: Any) -> None: ) with ui.row(): self.chart_envelopes = ui.chart(AnalyzeEnvelopes(self.selected_node).chart()) - self.chart_etats_enveloppes = ui.chart(timeline.chartOnTimeline("Evolution des états d'enveloppes",timeline._log_env_states,{ - "Unknown": "#434348", - "Closed": "#999999", - "Locked": "#F94144", - "Taxed": "#F9C74F", - "Free": "#7BB151", - }) if timeline else {}) - self.chart_enveloppes = ui.chart(timeline.chartOnTimeline("Evolution des enveloppes",timeline._log_enveloppe_values) if timeline else {}) - self.chart_asset_classes = ui.chart(timeline.chartOnTimeline("Evolution des classes d'actifs",timeline._log_assets_classes_values,AnalyzeAssetClasses.ASSET_COLORS_FINARY) if timeline else {}) - self.chart_subasset_classes = ui.chart(timeline.chartOnTimeline("Evolution des sous-classes d'actifs",timeline._log_assets_subclasses_values,AnalyzeSubAssetClasses.SUBASSET_COLORS_FINARY) if timeline else {}) - self.chart_lines = ui.chart(timeline.chartOnTimeline("Evolution des lignes du portefeuille", timeline._log_lines_values, visible_by_default=False) if timeline else {}) + self.chart_etats_enveloppes = ui.chart( + timeline.chartOnTimeline( + "Evolution des états d'enveloppes", + timeline._log_env_states, + { + "Unknown": "#434348", + "Closed": "#999999", + "Locked": "#F94144", + "Taxed": "#F9C74F", + "Free": "#7BB151", + }, + ) + if timeline + else {} + ) + self.chart_enveloppes = ui.chart( + timeline.chartOnTimeline("Evolution des enveloppes", timeline._log_enveloppe_values) + if timeline + else {} + ) + self.chart_asset_classes = ui.chart( + timeline.chartOnTimeline( + "Evolution des classes d'actifs", + timeline._log_assets_classes_values, + AnalyzeAssetClasses.ASSET_COLORS_FINARY, + ) + if timeline + else {} + ) + self.chart_subasset_classes = ui.chart( + timeline.chartOnTimeline( + "Evolution des sous-classes d'actifs", + timeline._log_assets_subclasses_values, + AnalyzeSubAssetClasses.SUBASSET_COLORS_FINARY, + ) + if timeline + else {} + ) + self.chart_lines = ui.chart( + timeline.chartOnTimeline( + "Evolution des lignes du portefeuille", + timeline._log_lines_values, + visible_by_default=False, + ) + if timeline + else {} + ) ui.run( title="Finalynx Dashboard", diff --git a/finalynx/simulator/timeline.py b/finalynx/simulator/timeline.py index 4a43830..1f4fdb6 100644 --- a/finalynx/simulator/timeline.py +++ b/finalynx/simulator/timeline.py @@ -1,15 +1,16 @@ from dataclasses import dataclass -from datetime import date, datetime +from datetime import date +from datetime import datetime from datetime import timedelta from typing import Any from typing import Dict from typing import List from typing import Optional + from finalynx.analyzer.asset_class import AnalyzeAssetClasses from finalynx.analyzer.envelopes import AnalyzeEnvelopes -from finalynx.analyzer.lines import AnalyzeLines - from finalynx.analyzer.investment_state import AnalyzeInvestmentStates +from finalynx.analyzer.lines import AnalyzeLines from finalynx.analyzer.subasset_class import AnalyzeSubAssetClasses from finalynx.portfolio.bucket import Bucket from finalynx.portfolio.constants import AssetClass @@ -42,13 +43,14 @@ class Simulation: # Whether to print the portfolio state in the console on each step of the simulation print_each_step: bool = False - + # Display the portfolio's worth in the console every `step` years step_years: int = 5 # Record the portfolio stats on each day of the simulation 'DAY', 'MONTH', 'YEAR' metrics_record_freqency: str = "MONTH" + class Timeline: """Main simulation engine to execute programmed actions on your portfolio.""" @@ -105,7 +107,7 @@ def step_until(self, target_date: date) -> None: """Execute all events until the specified date is reached.""" assert self.current_date < target_date, "Target date must be in the future." - #Enregistrement de la situation de démarrage du Portefeuille + # Enregistrement de la situation de démarrage du Portefeuille self._record_metrics() while self.current_date < target_date and not self.is_finished: @@ -129,7 +131,7 @@ def step(self) -> bool: if next_event.planned_date in self._log_events: self._log_events[next_event.planned_date].append(next_event.name) else: - self._log_events[next_event.planned_date]=[next_event.name] + self._log_events[next_event.planned_date] = [next_event.name] # Recalculate the amounts for shared folders for bucket in self._buckets: @@ -142,9 +144,19 @@ def step(self) -> bool: self._sort_events() # Record the metrics if the year changed - #if next_event.planned_date.year != self.current_date.year: + # if next_event.planned_date.year != self.current_date.year: # self._record_metrics() - if (self.simulation.metrics_record_freqency == "DAY" and next_event.planned_date != self.current_date) or (self.simulation.metrics_record_freqency == "YEAR" and next_event.planned_date.year != self.current_date.year) or (self.simulation.metrics_record_freqency == "MONTH" and next_event.planned_date.month != self.current_date.month): + if ( + (self.simulation.metrics_record_freqency == "DAY" and next_event.planned_date != self.current_date) + or ( + self.simulation.metrics_record_freqency == "YEAR" + and next_event.planned_date.year != self.current_date.year + ) + or ( + self.simulation.metrics_record_freqency == "MONTH" + and next_event.planned_date.month != self.current_date.month + ) + ): self.current_date = next_event.planned_date self._record_metrics() @@ -179,8 +191,8 @@ def _record_metrics(self) -> None: if key in self._log_enveloppe_values: self._log_enveloppe_values[key].append(value) else: - self._log_enveloppe_values[key]=[value] - + self._log_enveloppe_values[key] = [value] + for key, value in AnalyzeAssetClasses(self._portfolio).analyzeTime(self.current_date).items(): self._log_assets_classes_values[key].append(value) @@ -188,43 +200,57 @@ def _record_metrics(self) -> None: if key in self._log_assets_subclasses_values: self._log_assets_subclasses_values[key].append(value) else: - self._log_assets_subclasses_values[key]=[value] + self._log_assets_subclasses_values[key] = [value] for key, value in AnalyzeLines(self._portfolio).analyzeTime(self.current_date).items(): if key in self._log_lines_values: self._log_lines_values[key].append(value) else: - self._log_lines_values[key]=[value] + self._log_lines_values[key] = [value] else: - #On doit remplacer les valeurs stockées par les nouvelles sans créer 2 fois l'enregistrement + # On doit remplacer les valeurs stockées par les nouvelles sans créer 2 fois l'enregistrement ident = self._log_dates.index(self.current_date) # Record the envelope states and their amounts at this date for key, value in AnalyzeInvestmentStates(self._portfolio).analyze(self.current_date).items(): - self._log_env_states[key][-1]=value + self._log_env_states[key][-1] = value for key, value in AnalyzeEnvelopes(self._portfolio).analyzeTime(self.current_date).items(): if key in self._log_enveloppe_values: - self._log_enveloppe_values[key][-1]=value + self._log_enveloppe_values[key][-1] = value else: - self._log_enveloppe_values[key]=[value] + self._log_enveloppe_values[key] = [value] for key, value in AnalyzeAssetClasses(self._portfolio).analyzeTime(self.current_date).items(): - self._log_assets_classes_values[key][-1]=value + self._log_assets_classes_values[key][-1] = value for key, value in AnalyzeSubAssetClasses(self._portfolio).analyzeTime(self.current_date).items(): if key in self._log_assets_subclasses_values: - self._log_assets_subclasses_values[key][-1]=value + self._log_assets_subclasses_values[key][-1] = value else: - self._log_assets_subclasses_values[key]=[value] + self._log_assets_subclasses_values[key] = [value] for key, value in AnalyzeLines(self._portfolio).analyzeTime(self.current_date).items(): if key in self._log_lines_values: - self._log_lines_values[key][-1]=value + self._log_lines_values[key][-1] = value else: - self._log_lines_values[key]=[value] - - def chartOnTimeline(self, title: str, valuesToGraph: Dict[str, List[float]], colors: Dict[str,str]={}, visible_by_default: bool = True) -> Dict[str, Any]: + self._log_lines_values[key] = [value] + + def chartOnTimeline( + self, + title: str, + valuesToGraph: Dict[str, List[float]], + colors: Dict[str, str] = {}, + visible_by_default: bool = True, + ) -> Dict[str, Any]: """Plot a Highcharts chart of the portfolio's caracteristics and amounts over time.""" - #assert self._log_enveloppe_values, "Run the simulation before charting." + # assert self._log_enveloppe_values, "Run the simulation before charting." return { - "chart": {"plotBackgroundColor": None, "plotBorderWidth": None, "plotShadow": False, "type": "area", "zooming": {"type": 'xy' }, "height":1200, "width":1000}, + "chart": { + "plotBackgroundColor": None, + "plotBorderWidth": None, + "plotShadow": False, + "type": "area", + "zooming": {"type": "xy"}, + "height": 1200, + "width": 1000, + }, "title": {"text": title, "align": "center"}, "plotOptions": { "area": { @@ -239,35 +265,36 @@ def chartOnTimeline(self, title: str, valuesToGraph: Dict[str, List[float]], col "name": key, "data": self.convertDataSeries(value), "visible": visible_by_default, - "color": colors[key] if (key in colors) else {None} + "color": colors[key] if (key in colors) else {None}, } for key, value in valuesToGraph.items() ], - "xAxis": {"type": 'datetime'}, + "xAxis": {"type": "datetime"}, "yAxis": {"crosshair": True}, "tooltip": { - "xDateFormat": '%m %Y', - "pointFormat": '{point.x:%e/%m/%Y}: {point.y:,.0f}€
', - "footerFormat": '{series.name}', + "xDateFormat": "%m %Y", + "pointFormat": "{point.x:%e/%m/%Y}: {point.y:,.0f}€
", + "footerFormat": "{series.name}", }, "credits": {"enabled": False}, } - def convertDataSeries(self, data: [float])->[Any]: + def convertDataSeries(self, data: [float]) -> [Any]: """Convert DataSeries in a time series format to allow non regular data""" - res=[] + res = [] i = 0 while i < len(data): - if (self._log_dates[i] in self._log_events): - evenements = "* "+"
* ".join(self._log_events[self._log_dates[i]]) - else: + if self._log_dates[i] in self._log_events: + evenements = "* " + "
* ".join(self._log_events[self._log_dates[i]]) + else: evenements = "" - point = {"x": datetime.combine(self._log_dates[i], datetime.min.time()).timestamp() * 1000, - "y": data[i], - "name": evenements - } + point = { + "x": datetime.combine(self._log_dates[i], datetime.min.time()).timestamp() * 1000, + "y": data[i], + "name": evenements, + } res.append(point) - i+=1 + i += 1 return res def _sort_events(self) -> None: diff --git a/finalynx/usage.py b/finalynx/usage.py index 1259a59..98a409c 100644 --- a/finalynx/usage.py +++ b/finalynx/usage.py @@ -57,6 +57,6 @@ def main_filter(message: str) -> str: --sim-steps=int Display the simulated portfolio's worth every X years, defaults to 5 --future Print the portfolio after the simulation has finished --each-step Print the portfolio for each step of the simulation - --metric-frequency Record the portfolio stats on each day of the simulation 'DAY', 'MONTH', 'YEAR' + --metric-frequency Record the portfolio stats on each day of the simulation 'DAY', 'MONTH', 'YEAR' """ From 465d4e73e828b9f1a3ff6533a83d74f4ee5c44f8 Mon Sep 17 00:00:00 2001 From: MadeInPierre Date: Sat, 13 Jan 2024 15:50:33 +0100 Subject: [PATCH 10/17] chore: update full example --- examples/full_example.py | 118 +++++++++++++++++++++++--------- finalynx/fetch/source_finary.py | 2 +- 2 files changed, 85 insertions(+), 35 deletions(-) diff --git a/examples/full_example.py b/examples/full_example.py index f473f4b..cce8abf 100755 --- a/examples/full_example.py +++ b/examples/full_example.py @@ -15,8 +15,22 @@ # noreorder from datetime import date from rich import inspect, print, pretty, traceback # noqa -from finalynx import TargetRange, TargetMin, TargetMax, TargetRatio, TargetGlobalRatio # noqa -from finalynx import Sidecar, Folder, Line, LinePerf, Bucket, SharedFolder, Portfolio, FolderDisplay # noqa +from finalynx import ( + TargetRange, + TargetMin, + TargetMax, + TargetRatio, +) # noqa +from finalynx import ( + Sidecar, + Folder, + Line, + LinePerf, + Bucket, + SharedFolder, + Portfolio, + FolderDisplay, +) # noqa from finalynx import Envelope, PEA, PEE, AV, PER from finalynx import AssetClass, AssetSubclass from finalynx import Simulation, AddLineAmount, Event, Salary @@ -31,24 +45,36 @@ """ (Optional) Custom shortcuts in variables used below to control the config quickly. """ - short_display = FolderDisplay.COLLAPSED # Display style for all short-term folders - medium_term_amount = 20000 # Amount of money to keep for medium-term (i.e. Livrets in this config) + short_display = FolderDisplay.EXPANDED # Display style for all short-term folders + medium_term_amount = ( + 20000 # Amount of money to keep for medium-term (i.e. Livrets in this config) + ) date_retirement = date(2063, 7, 1) # Define envelopes used in the portfolio bank_lbp = Envelope("La Banque Postale", "LBP") bank_n26 = Envelope("N26", "N26") - bank_boursorama = Envelope("Boursorama", "BOU") + bank_boursorama = Envelope("BoursoBank", "BOU") - pea = PEA("Bourse Direct", "PEA", date(2022, 7, 1), key="PEA") - pee = PEE("Natixis", "PEE", date(2023, 4, 1), date_unlock=date(2023, 11, 22), key="PEE Natixis") + pea = PEA( + "Bourse Direct", "PEA", date(2022, 7, 1), key="MR LACLAU PIERRE (Compte PEA)" + ) + pee = PEE( + "Natixis", + "PEE", + date(2023, 4, 1), + date_unlock=date(2023, 11, 22), + key="Plan d'Epargne Entreprise", + ) av_linxea = AV("Linxea Spirit 2", "LIX", date(2022, 7, 1), key="LINXEA Spirit 2") av_goodvest = AV("Goodvest", "GOO", date(2022, 7, 1)) av_ramify = AV("Ramify", "RAM", date(2022, 7, 1), key="Ramify AV") per_linxea = PER("Linxea Spirit PER", "PER", date(2022, 7, 1), date_retirement) - per_prefon = PER("Prefon", "PRF", date(2022, 7, 1), date_retirement, key="Autres actifs") + per_prefon = PER( + "Prefon", "PRF", date(2022, 7, 1), date_retirement, key="Autres actifs" + ) at_home = Envelope("At Home", "PHY", key="Metaux precieux") @@ -249,27 +275,27 @@ asset_class=AssetClass.STOCK, asset_subclass=AssetSubclass.ETF, target=TargetRatio(50), - perf=LinePerf(8), + perf=LinePerf(6), children=[ Folder( "USA", target=TargetRatio(50), children=[ Line( - "[italic]PE500[/] - SP500", - key="7030926", + "SP500 [italic](PE500)[/]", + key="13577960", target=TargetRatio(50), envelope=pea, ), Line( - "[italic]xxxxx[/] - SP500 ESG", - key="8804142", + "SP500 ESG [italic](xxxxx)[/]", + key="13578020", target=TargetRatio(30), envelope=av_linxea, ), Line( - "[italic]RS2K[/] - Russell 2000", - key="11358415", + "Russell 2000 [italic](RS2K)[/]", + key="13577964", target=TargetRatio(20), envelope=pea, ), @@ -280,14 +306,14 @@ target=TargetRatio(30), children=[ Line( - "[italic]PABZ[/] - Europe 600 ESG", - key="7492719", + "Europe 600 ESG [italic](PABZ)[/]", + key="13577963", target=TargetRatio(80), envelope=pea, ), Line( - "[italic]ETZ[/] - Europe 600", - key="7030927", + " Europe 600 [italic](ETZ)[/]", + key="13577961", target=TargetRatio(20), envelope=pea, ), @@ -298,19 +324,19 @@ target=TargetRatio(20), children=[ Line( - "[italic]PAEEM[/] - Emerging markets", - key="7036606", + "Emerging markets [italic](PAEEM)[/]", + key="13577962", target=TargetRatio(50), envelope=pea, ), Line( - "[italic]xxxxx[/] - Emerging markets ESG", + "Emerging markets ESG [italic](xxxxx)[/]", key="8804145", target=TargetRatio(30), envelope=av_linxea, ), Line( - "[italic]PTPXE[/] - Japon", + "Japon [italic](PTPXE)[/]", key="", target=TargetRatio(20), newline=True, @@ -389,6 +415,27 @@ ), ], ), + Folder( + "Bloqué", + perf=LinePerf(0, skip=True), + children=[ + Line( + "PEE (à récupérer)", + AssetClass.CASH, + AssetSubclass.MONETARY, + key="13009544", + envelope=pee, + ), + Line( + "Shares To Win Stellantis (à récupérer)", + AssetClass.STOCK, + AssetSubclass.STOCK_SHARE, + key="13417344", + envelope=pee, + newline=True, + ), + ], + ), Folder( "Retraite", perf=LinePerf(0, skip=True), @@ -430,18 +477,11 @@ key="CCP Boursorama", envelope=bank_boursorama, ), - Line( - "PEE (à récupérer)", - AssetClass.CASH, - AssetSubclass.MONETARY, - key="10117145", - envelope=pee, - ), Line( "Liquidités PEA (à investir)", AssetClass.CASH, AssetSubclass.LIQUIDITY, - key="7024202", + key="13577959", envelope=pea, target=TargetMax(0), ), @@ -509,9 +549,19 @@ ], simulation=Simulation( events=[ - Salary(livreta, income=2300, expenses=1400, end_date=date(2024, 11, 30)), - Event(AddLineAmount(livreta, 3500), planned_date=date(2024, 4, 10), name="Prime"), - Event(AddLineAmount(livreta, 3500), planned_date=date(2025, 4, 10), name="Prime"), + Salary( + livreta, income=2300, expenses=1400, end_date=date(2024, 11, 30) + ), + Event( + AddLineAmount(livreta, 3500), + planned_date=date(2024, 4, 10), + name="Prime", + ), + Event( + AddLineAmount(livreta, 3500), + planned_date=date(2025, 4, 10), + name="Prime", + ), Salary( livreta, income=3500, diff --git a/finalynx/fetch/source_finary.py b/finalynx/fetch/source_finary.py index 62e5e1c..55e7085 100644 --- a/finalynx/fetch/source_finary.py +++ b/finalynx/fetch/source_finary.py @@ -179,7 +179,7 @@ def _fetch_data(self, tree: Tree) -> None: def _process_account(self, dict_account: Dict[str, Any], tree: Tree) -> None: account_name = dict_account["name"] - node = tree.add(account_name) + node = tree.add(account_name if not dict_account["fiats"] else dict_account["institution"]["name"]) for item in dict_account["fiats"]: subtype = dict_account["bank_account_type"]["subtype"] From 66feb956345983dfb37bc36a576a98f056a86f79 Mon Sep 17 00:00:00 2001 From: MadeInPierre Date: Sat, 13 Jan 2024 15:52:51 +0100 Subject: [PATCH 11/17] ci: manual update to v1.22.3 --- docs/conf.py | 2 +- finalynx/__meta__.py | 2 +- pyproject.toml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 92d3d21..e76d392 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,7 +22,7 @@ author = "Pierre Laclau" # The full version, including alpha/beta/rc tags -release = "1.22.2" +release = "1.22.3" # -- General configuration --------------------------------------------------- diff --git a/finalynx/__meta__.py b/finalynx/__meta__.py index 92d3599..c0a6679 100644 --- a/finalynx/__meta__.py +++ b/finalynx/__meta__.py @@ -6,7 +6,7 @@ Metadata information about Finalynx. This file is used by Fynalinx and updated by the CI/CD pipeline. """ -__version__ = "1.22.2" +__version__ = "1.22.3" __author__ = "Pierre Laclau (MadeInPierre)" diff --git a/pyproject.toml b/pyproject.toml index 68f3eea..4971ba1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "finalynx" -version = "1.22.2" +version = "1.22.3" description = "A command line investment assistant to organize your portfolio and simulate its future to reach your life goals." authors = ["MadeInPierre "] license = "GPLv3" @@ -56,7 +56,7 @@ build_command = "pip install poetry && poetry build" [tool.commitizen] name = "cz_conventional_commits" -version = "1.22.2" +version = "1.22.3" tag_format = "v$version" [tool.mypy] From 2e8a0b4bdf3c67fec786197f8204ed8c5a03cb3e Mon Sep 17 00:00:00 2001 From: gcoue Date: Sat, 13 Jan 2024 16:50:52 +0100 Subject: [PATCH 12/17] fix(fetch): change credit card to negative amount (#147) * Change credit card to negative amount * Rollback source_finary.py * Change credit card to negative amount * [pre-commit.ci lite] apply automatic fixes * Add a flag to allow the potfolio restitution for each simulation step * Add a flag to allow the potfolio restitution for each simulation step * fix: correct the recurrence function to be more precise on Yearly recurrence * [pre-commit.ci lite] apply automatic fixes * style: minor refactoring --- finalynx/assistant.py | 16 +++++++++------- finalynx/fetch/source_finary.py | 14 ++++++++++---- finalynx/simulator/recurrence.py | 3 ++- finalynx/simulator/timeline.py | 2 +- 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/finalynx/assistant.py b/finalynx/assistant.py index 606cbaf..ee80193 100644 --- a/finalynx/assistant.py +++ b/finalynx/assistant.py @@ -93,9 +93,6 @@ def __init__( self.buckets = buckets if buckets else [] self.envelopes = envelopes if envelopes else [] - # Storage for value of portfolio at each intermediate step - self.intermediate_value = [] - # Options that can either be set in the constructor or from the command line options, see --help self.ignore_orphans = ignore_orphans self.clear_cache = clear_cache @@ -128,6 +125,9 @@ def __init__( # Initialize the simulation timeline with the initial user events self._timeline = Timeline(simulation, self.portfolio, self.buckets) if simulation else None + # Store the portfolio renders for each simulation date (if enabled) + self._timeline_renders: List[Any] = [] + def add_source(self, source: SourceBaseLine) -> None: """Register a source, either defined in your own config or from the available Finalynx sources using `from finalynx.fetch.source_any import SourceAny`.""" @@ -241,9 +241,9 @@ def run(self) -> None: # Add the simulation summary to the performance panel in the console dict_panels["performance"].add(self.simulate()) - # If enabled by the user, print the each_step portfolio during the simulation + # If enabled by the user, print the portfolio at each simulation date if self.simulation.print_each_step: - for element in self.intermediate_value: + for element in self._timeline_renders: renders.append(element) # If enabled by the user, print the final portfolio after the simulation @@ -311,11 +311,13 @@ def append_worth(year: int, amount: float) -> None: self._timeline.goto(date(year, 12, 31)) if (year - date.today().year) % self.simulation.step_years == 0: + # Append the portfolio's worth to the Worth tree append_worth(year, self.portfolio.get_amount()) + + # Render each intermediate simulation step if self.simulation.print_each_step: - # Storage for each intermediate simulation step title = "Your portfolio in [bold]" + str(year) + "-12-31:[/]" - self.intermediate_value.append(Panel(self.render_mainframe(), title=title)) + self._timeline_renders.append(Panel(self.render_mainframe(), title=title)) # Run until the end date and append the final result self._timeline.run() diff --git a/finalynx/fetch/source_finary.py b/finalynx/fetch/source_finary.py index 55e7085..afb9870 100644 --- a/finalynx/fetch/source_finary.py +++ b/finalynx/fetch/source_finary.py @@ -84,7 +84,10 @@ def _authenticate(self) -> Optional[Session]: if os.path.exists(finary_uapi.constants.COOKIE_FILENAME): os.remove(finary_uapi.constants.COOKIE_FILENAME) if os.path.exists(finary_uapi.constants.CREDENTIAL_FILE): - if not Confirm.ask("Reuse saved credentials? Otherwise, they will also be deleted.", default=True): + if not Confirm.ask( + "Reuse saved credentials? Otherwise, they will also be deleted.", + default=True, + ): os.remove(finary_uapi.constants.CREDENTIAL_FILE) # Get the user credentials if there's no session yet (through environment variables or manual input) @@ -171,7 +174,10 @@ def _fetch_data(self, tree: Tree) -> None: raise ValueError("Finary signin failed.") # Call the API and parse the response into `FetchLine` instances - with console.status(f"[bold {TH().ACCENT}]Fetching investments from Finary...", spinner_style=TH().ACCENT): + with console.status( + f"[bold {TH().ACCENT}]Fetching investments from Finary...", + spinner_style=TH().ACCENT, + ): response = ff.get_holdings_accounts(session) if response["message"] == "OK": for dict_account in response["result"]: @@ -182,11 +188,11 @@ def _process_account(self, dict_account: Dict[str, Any], tree: Tree) -> None: node = tree.add(account_name if not dict_account["fiats"] else dict_account["institution"]["name"]) for item in dict_account["fiats"]: - subtype = dict_account["bank_account_type"]["subtype"] - if subtype == "credit": + if dict_account["bank_account_type"]["subtype"] == "credit": amount = -item["display_current_value"] else: amount = item["display_current_value"] + self._register_fetchline( tree_node=node, name=account_name, diff --git a/finalynx/simulator/recurrence.py b/finalynx/simulator/recurrence.py index a0d372c..fd0f747 100644 --- a/finalynx/simulator/recurrence.py +++ b/finalynx/simulator/recurrence.py @@ -36,7 +36,8 @@ def __init__( months = months if months is not None else 0 years = years if years is not None else 0 - self._delta = timedelta(days, weeks=4 * months + 52 * years) + # Add decimals to stay on the same day (otherwise Yearly goes to 30/12) + self._delta = timedelta(days, weeks=4.3452 * months + 52.1429 * years + 0.1429) def _next_date(self, current_date: date) -> date: return current_date + self._delta diff --git a/finalynx/simulator/timeline.py b/finalynx/simulator/timeline.py index 1f4fdb6..7651074 100644 --- a/finalynx/simulator/timeline.py +++ b/finalynx/simulator/timeline.py @@ -41,7 +41,7 @@ class Simulation: # Whether to print the final portfolio state in the console after the simulation print_final: bool = False - # Whether to print the portfolio state in the console on each step of the simulation + # Whether to print the final portfolio state in the console after the simulation print_each_step: bool = False # Display the portfolio's worth in the console every `step` years From a6608c075815c3d5b47c6899d39bad75d5b19351 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 13 Jan 2024 15:53:52 +0000 Subject: [PATCH 13/17] 1.22.4 Automatically generated by python-semantic-release --- CHANGELOG.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 012fa55..86f5034 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,41 @@ +## v1.22.4 (2024-01-13) + +### Chore + +* chore: update full example ([`00138dd`](https://github.com/MadeInPierre/finalynx/commit/00138dd2fcaf2226402b5d1881ee9993624c8cc7)) + +### Ci + +* ci: manual update to v1.22.3 ([`c2d7d82`](https://github.com/MadeInPierre/finalynx/commit/c2d7d8286ad289222fecafcdf4456356a7ffcebc)) + +* ci: use trusted auth for PyPI publishing ([`ea2a814`](https://github.com/MadeInPierre/finalynx/commit/ea2a814b091d3c47987645b1ce9ac1bead5b61f8)) + +### Fix + +* fix(fetch): change credit card to negative amount (#147) + +* Change credit card to negative amount + +* Rollback source_finary.py + +* Change credit card to negative amount + +* [pre-commit.ci lite] apply automatic fixes + +* Add a flag to allow the potfolio restitution for each simulation step + +* Add a flag to allow the potfolio restitution for each simulation step + +* fix: correct the recurrence function to be more precise on Yearly recurrence + +* [pre-commit.ci lite] apply automatic fixes + +* style: minor refactoring ([`d9478db`](https://github.com/MadeInPierre/finalynx/commit/d9478db0bcd4db01887ddee5ed542dcc6204116b)) + + ## v1.22.3 (2023-09-18) ### Chore From 9c5252bc8f95734c67978ee91ca47a80cf7cde9d Mon Sep 17 00:00:00 2001 From: MadeInPierre Date: Sat, 13 Jan 2024 17:05:55 +0100 Subject: [PATCH 14/17] ci: add permissions for semantic release --- .github/workflows/semantic-release.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/semantic-release.yml b/.github/workflows/semantic-release.yml index 41ec049..e3dcb36 100644 --- a/.github/workflows/semantic-release.yml +++ b/.github/workflows/semantic-release.yml @@ -55,6 +55,10 @@ jobs: needs: pre-commit if: github.event_name == 'push' && github.ref == 'refs/heads/main' && !contains(github.event.head_commit.message, 'chore(release):') runs-on: ubuntu-latest + permissions: + issues: write + id-token: write + contents: write steps: - uses: actions/setup-python@v2 with: From 1d365a4f72a108c10d2791959c62109c10a63d23 Mon Sep 17 00:00:00 2001 From: MadeInPierre Date: Sat, 13 Jan 2024 17:10:55 +0100 Subject: [PATCH 15/17] ci: manual bump to v1.22.4 --- docs/conf.py | 2 +- finalynx/__meta__.py | 2 +- pyproject.toml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index e76d392..23cc2c2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,7 +22,7 @@ author = "Pierre Laclau" # The full version, including alpha/beta/rc tags -release = "1.22.3" +release = "1.22.4" # -- General configuration --------------------------------------------------- diff --git a/finalynx/__meta__.py b/finalynx/__meta__.py index c0a6679..3a76528 100644 --- a/finalynx/__meta__.py +++ b/finalynx/__meta__.py @@ -6,7 +6,7 @@ Metadata information about Finalynx. This file is used by Fynalinx and updated by the CI/CD pipeline. """ -__version__ = "1.22.3" +__version__ = "1.22.4" __author__ = "Pierre Laclau (MadeInPierre)" diff --git a/pyproject.toml b/pyproject.toml index 4971ba1..e72f1dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "finalynx" -version = "1.22.3" +version = "1.22.4" description = "A command line investment assistant to organize your portfolio and simulate its future to reach your life goals." authors = ["MadeInPierre "] license = "GPLv3" @@ -56,7 +56,7 @@ build_command = "pip install poetry && poetry build" [tool.commitizen] name = "cz_conventional_commits" -version = "1.22.3" +version = "1.22.4" tag_format = "v$version" [tool.mypy] From f961f039975761fe8fb2d67ed694cde46acbb7f0 Mon Sep 17 00:00:00 2001 From: MadeInPierre Date: Sat, 13 Jan 2024 18:13:37 +0100 Subject: [PATCH 16/17] refactor: fix lint errors --- finalynx/analyzer/envelopes.py | 2 -- finalynx/analyzer/subasset_class.py | 2 -- finalynx/simulator/timeline.py | 3 ++- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/finalynx/analyzer/envelopes.py b/finalynx/analyzer/envelopes.py index 329bf41..423fd6c 100644 --- a/finalynx/analyzer/envelopes.py +++ b/finalynx/analyzer/envelopes.py @@ -2,8 +2,6 @@ from typing import Any from typing import Dict -from finalynx.portfolio.constants import EnvelopeClass - from ..portfolio import Folder from ..portfolio import Line from ..portfolio import Node diff --git a/finalynx/analyzer/subasset_class.py b/finalynx/analyzer/subasset_class.py index 7bd9fc8..526d5fb 100644 --- a/finalynx/analyzer/subasset_class.py +++ b/finalynx/analyzer/subasset_class.py @@ -2,8 +2,6 @@ from typing import Any from typing import Dict -import numpy as np - from ..portfolio import AssetClass from ..portfolio import AssetSubclass from ..portfolio import Folder diff --git a/finalynx/simulator/timeline.py b/finalynx/simulator/timeline.py index 7651074..f34cb21 100644 --- a/finalynx/simulator/timeline.py +++ b/finalynx/simulator/timeline.py @@ -209,7 +209,8 @@ def _record_metrics(self) -> None: self._log_lines_values[key] = [value] else: # On doit remplacer les valeurs stockées par les nouvelles sans créer 2 fois l'enregistrement - ident = self._log_dates.index(self.current_date) + # ident = self._log_dates.index(self.current_date) # TODO + # Record the envelope states and their amounts at this date for key, value in AnalyzeInvestmentStates(self._portfolio).analyze(self.current_date).items(): self._log_env_states[key][-1] = value From b095b3937f0bea6c3e583141df50428b6abd5482 Mon Sep 17 00:00:00 2001 From: MadeInPierre Date: Mon, 15 Jan 2024 17:26:56 +0100 Subject: [PATCH 17/17] refactor: small tweaks & simplifications --- finalynx/analyzer/asset_class.py | 11 ++-- .../{subasset_class.py => asset_subclass.py} | 17 +++---- finalynx/analyzer/envelopes.py | 32 ------------ finalynx/analyzer/lines.py | 9 ++-- finalynx/assistant.py | 2 +- finalynx/dashboard/dashboard.py | 22 ++++---- finalynx/simulator/timeline.py | 50 ++++++++----------- 7 files changed, 51 insertions(+), 92 deletions(-) rename finalynx/analyzer/{subasset_class.py => asset_subclass.py} (90%) diff --git a/finalynx/analyzer/asset_class.py b/finalynx/analyzer/asset_class.py index ea3bebe..500cddf 100644 --- a/finalynx/analyzer/asset_class.py +++ b/finalynx/analyzer/asset_class.py @@ -1,4 +1,3 @@ -from datetime import date from typing import Any from typing import Dict @@ -70,15 +69,15 @@ class AnalyzeAssetClasses(Analyzer): def analyze(self) -> Dict[str, Any]: """:returns: A dictionary with keys as the asset class names and values as the - sum of investments corresponding to each class.""" + sum of investments corresponding to each class. Two-layer dictionary with classes and subclasses.""" return self._recursive_merge(self.node) - def analyzeTime(self, target_date: date) -> Dict[str, float]: + def analyze_flat(self) -> Dict[str, float]: """:returns: A dictionary with keys as the asset class names and values as the sum of investments corresponding to each class.""" - return self._recursive_mergeTime(self.node, target_date) + return self._recursive_merge_flat(self.node) - def _recursive_mergeTime(self, node: Node, target_date: date) -> Dict[str, Any]: + def _recursive_merge_flat(self, node: Node) -> Dict[str, Any]: """Internal method for recursive searching.""" total = {c.value: 0.0 for c in AssetClass} @@ -90,7 +89,7 @@ def _recursive_mergeTime(self, node: Node, target_date: date) -> Dict[str, Any]: # Folders merge what the children return elif isinstance(node, Folder): for child in node.children: - for key, value in self._recursive_mergeTime(child, target_date).items(): + for key, value in self._recursive_merge_flat(child).items(): total[key] += value return total diff --git a/finalynx/analyzer/subasset_class.py b/finalynx/analyzer/asset_subclass.py similarity index 90% rename from finalynx/analyzer/subasset_class.py rename to finalynx/analyzer/asset_subclass.py index 526d5fb..6b34e9d 100644 --- a/finalynx/analyzer/subasset_class.py +++ b/finalynx/analyzer/asset_subclass.py @@ -1,4 +1,3 @@ -from datetime import date from typing import Any from typing import Dict @@ -10,7 +9,7 @@ from .analyzer import Analyzer -class AnalyzeSubAssetClasses(Analyzer): +class AnalyzeAssetSubclasses(Analyzer): """Aims to agglomerate the children's Sub asset classes and return the amount represented by each Sub asset class. :returns: a dictionary with Sub asset classes as keys and the @@ -102,16 +101,16 @@ class AnalyzeSubAssetClasses(Analyzer): } def analyze(self) -> Dict[str, Any]: - """:returns: A dictionary with keys as the asset class names and values as the - sum of investments corresponding to each class.""" + """:returns: A dictionary with keys as the asset class names and values as the sum of + investments corresponding to each class. Two-layer dictionary with classes and subclasses.""" return self._recursive_merge(self.node) - def analyzeTime(self, target_date: date) -> Dict[str, float]: + def analyze_flat(self) -> Dict[str, float]: """:returns: A dictionary with keys as the Sub asset class names and values as the - sum of investments corresponding to each class.""" - return self._recursive_mergeTime(self.node, target_date) + sum of investments corresponding to each subclass.""" + return self._recursive_merge_flat(self.node) - def _recursive_mergeTime(self, node: Node, target_date: date) -> Dict[str, Any]: + def _recursive_merge_flat(self, node: Node) -> Dict[str, Any]: """Internal method for recursive searching.""" total = {} @@ -123,7 +122,7 @@ def _recursive_mergeTime(self, node: Node, target_date: date) -> Dict[str, Any]: # Folders merge what the children return elif isinstance(node, Folder): for child in node.children: - for key, value in self._recursive_mergeTime(child, target_date).items(): + for key, value in self._recursive_merge_flat(child).items(): if key in total.keys(): total[key] += value else: diff --git a/finalynx/analyzer/envelopes.py b/finalynx/analyzer/envelopes.py index 423fd6c..7486b17 100644 --- a/finalynx/analyzer/envelopes.py +++ b/finalynx/analyzer/envelopes.py @@ -1,4 +1,3 @@ -from datetime import date from typing import Any from typing import Dict @@ -20,37 +19,6 @@ def analyze(self) -> Dict[str, float]: sum of investments corresponding to each class.""" return self._recursive_merge(self.node) - def analyzeTime(self, target_date: date) -> Dict[str, float]: - """:returns: A dictionary with keys as the asset class names and values as the - sum of investments corresponding to each class.""" - return self._recursive_mergeTime(self.node, target_date) - - def _recursive_mergeTime(self, node: Node, target_date: date) -> Dict[str, Any]: - """Internal method for recursive searching.""" - total = {} - - # Lines simply return their own amount - if isinstance(node, Line): - if node.envelope: - total[node.envelope.name] = node.get_amount() - else: - total["Unknown"] = node.get_amount() - return total - - # Folders merge what the children return - elif isinstance(node, Folder): - for child in node.children: - for key, value in self._recursive_mergeTime(child, target_date).items(): - if key in total.keys(): - total[key] += value - else: - total[key] = value - return total - - # Safeguard for future versions - else: - raise ValueError(f"Unknown node type '{type(node)}'.") - def _recursive_merge(self, node: Node) -> Dict[str, float]: """Internal method for recursive searching.""" total = {} diff --git a/finalynx/analyzer/lines.py b/finalynx/analyzer/lines.py index f93f540..7e96b37 100644 --- a/finalynx/analyzer/lines.py +++ b/finalynx/analyzer/lines.py @@ -1,4 +1,3 @@ -from datetime import date from typing import Any from typing import Dict @@ -15,12 +14,12 @@ class AnalyzeLines(Analyzer): corresponding total amount contained in the children. """ - def analyzeTime(self, target_date: date) -> Dict[str, float]: + def analyze(self) -> Dict[str, float]: """:returns: A dictionary with keys as the asset class names and values as the sum of investments corresponding to each class.""" - return self._recursive_mergeTime(self.node, target_date) + return self._recursive_merge(self.node) - def _recursive_mergeTime(self, node: Node, target_date: date) -> Dict[str, Any]: + def _recursive_merge(self, node: Node) -> Dict[str, Any]: """Internal method for recursive searching.""" total = {} @@ -35,7 +34,7 @@ def _recursive_mergeTime(self, node: Node, target_date: date) -> Dict[str, Any]: # Folders merge what the children return elif isinstance(node, Folder): for child in node.children: - for key, value in self._recursive_mergeTime(child, target_date).items(): + for key, value in self._recursive_merge(child).items(): if key in total.keys(): total[key] += value else: diff --git a/finalynx/assistant.py b/finalynx/assistant.py index ee80193..4ce8693 100644 --- a/finalynx/assistant.py +++ b/finalynx/assistant.py @@ -198,7 +198,7 @@ def _parse_args(self) -> None: if args["--sim-steps"] and self.simulation: self.simulation.step_years = int(args["--sim-steps"]) if args["--metric-frequency"] and self.simulation: - self.simulation.metrics_record_freqency = str(args["--metric-frequency"]) + self.simulation.metrics_record_frequency = str(args["--metric-frequency"]) if args["--theme"]: theme_name = str(args["--theme"]) if theme_name not in finalynx.theme.AVAILABLE_THEMES: diff --git a/finalynx/dashboard/dashboard.py b/finalynx/dashboard/dashboard.py index 2eabd14..767726b 100644 --- a/finalynx/dashboard/dashboard.py +++ b/finalynx/dashboard/dashboard.py @@ -10,9 +10,9 @@ from typing import Set from finalynx.analyzer.asset_class import AnalyzeAssetClasses +from finalynx.analyzer.asset_subclass import AnalyzeAssetSubclasses from finalynx.analyzer.envelopes import AnalyzeEnvelopes from finalynx.analyzer.investment_state import AnalyzeInvestmentStates -from finalynx.analyzer.subasset_class import AnalyzeSubAssetClasses from finalynx.portfolio.folder import Folder from finalynx.portfolio.folder import FolderDisplay from finalynx.portfolio.line import Line @@ -183,8 +183,8 @@ def _on_select_color_map(data: Any) -> None: with ui.row(): self.chart_envelopes = ui.chart(AnalyzeEnvelopes(self.selected_node).chart()) self.chart_etats_enveloppes = ui.chart( - timeline.chartOnTimeline( - "Evolution des états d'enveloppes", + timeline.chart_timeline( + "Envelope States Evolution", timeline._log_env_states, { "Unknown": "#434348", @@ -198,13 +198,13 @@ def _on_select_color_map(data: Any) -> None: else {} ) self.chart_enveloppes = ui.chart( - timeline.chartOnTimeline("Evolution des enveloppes", timeline._log_enveloppe_values) + timeline.chart_timeline("Envelopes Evolution", timeline._log_enveloppe_values) if timeline else {} ) self.chart_asset_classes = ui.chart( - timeline.chartOnTimeline( - "Evolution des classes d'actifs", + timeline.chart_timeline( + "Asset Classes Evolution", timeline._log_assets_classes_values, AnalyzeAssetClasses.ASSET_COLORS_FINARY, ) @@ -212,17 +212,17 @@ def _on_select_color_map(data: Any) -> None: else {} ) self.chart_subasset_classes = ui.chart( - timeline.chartOnTimeline( - "Evolution des sous-classes d'actifs", + timeline.chart_timeline( + "Asset Subclasses Evolution", timeline._log_assets_subclasses_values, - AnalyzeSubAssetClasses.SUBASSET_COLORS_FINARY, + AnalyzeAssetSubclasses.SUBASSET_COLORS_FINARY, ) if timeline else {} ) self.chart_lines = ui.chart( - timeline.chartOnTimeline( - "Evolution des lignes du portefeuille", + timeline.chart_timeline( + "Line-by-line Evolution", timeline._log_lines_values, visible_by_default=False, ) diff --git a/finalynx/simulator/timeline.py b/finalynx/simulator/timeline.py index f34cb21..2eb56bb 100644 --- a/finalynx/simulator/timeline.py +++ b/finalynx/simulator/timeline.py @@ -8,10 +8,10 @@ from typing import Optional from finalynx.analyzer.asset_class import AnalyzeAssetClasses +from finalynx.analyzer.asset_subclass import AnalyzeAssetSubclasses from finalynx.analyzer.envelopes import AnalyzeEnvelopes from finalynx.analyzer.investment_state import AnalyzeInvestmentStates from finalynx.analyzer.lines import AnalyzeLines -from finalynx.analyzer.subasset_class import AnalyzeSubAssetClasses from finalynx.portfolio.bucket import Bucket from finalynx.portfolio.constants import AssetClass from finalynx.portfolio.envelope import EnvelopeState @@ -48,7 +48,7 @@ class Simulation: step_years: int = 5 # Record the portfolio stats on each day of the simulation 'DAY', 'MONTH', 'YEAR' - metrics_record_freqency: str = "MONTH" + metrics_record_frequency: str = "MONTH" class Timeline: @@ -144,18 +144,11 @@ def step(self) -> bool: self._sort_events() # Record the metrics if the year changed - # if next_event.planned_date.year != self.current_date.year: - # self._record_metrics() + _freq = self.simulation.metrics_record_frequency if ( - (self.simulation.metrics_record_freqency == "DAY" and next_event.planned_date != self.current_date) - or ( - self.simulation.metrics_record_freqency == "YEAR" - and next_event.planned_date.year != self.current_date.year - ) - or ( - self.simulation.metrics_record_freqency == "MONTH" - and next_event.planned_date.month != self.current_date.month - ) + (_freq == "DAY" and next_event.planned_date != self.current_date) + or (_freq == "YEAR" and next_event.planned_date.year != self.current_date.year) + or (_freq == "MONTH" and next_event.planned_date.month != self.current_date.month) ): self.current_date = next_event.planned_date self._record_metrics() @@ -187,52 +180,53 @@ def _record_metrics(self) -> None: for key, value in AnalyzeInvestmentStates(self._portfolio).analyze(self.current_date).items(): self._log_env_states[key].append(value) - for key, value in AnalyzeEnvelopes(self._portfolio).analyzeTime(self.current_date).items(): + for key, value in AnalyzeEnvelopes(self._portfolio).analyze().items(): if key in self._log_enveloppe_values: self._log_enveloppe_values[key].append(value) else: self._log_enveloppe_values[key] = [value] - for key, value in AnalyzeAssetClasses(self._portfolio).analyzeTime(self.current_date).items(): + for key, value in AnalyzeAssetClasses(self._portfolio).analyze_flat().items(): self._log_assets_classes_values[key].append(value) - for key, value in AnalyzeSubAssetClasses(self._portfolio).analyzeTime(self.current_date).items(): + for key, value in AnalyzeAssetSubclasses(self._portfolio).analyze_flat().items(): if key in self._log_assets_subclasses_values: self._log_assets_subclasses_values[key].append(value) else: self._log_assets_subclasses_values[key] = [value] - for key, value in AnalyzeLines(self._portfolio).analyzeTime(self.current_date).items(): + for key, value in AnalyzeLines(self._portfolio).analyze().items(): if key in self._log_lines_values: self._log_lines_values[key].append(value) else: self._log_lines_values[key] = [value] else: - # On doit remplacer les valeurs stockées par les nouvelles sans créer 2 fois l'enregistrement - # ident = self._log_dates.index(self.current_date) # TODO - # Record the envelope states and their amounts at this date for key, value in AnalyzeInvestmentStates(self._portfolio).analyze(self.current_date).items(): self._log_env_states[key][-1] = value - for key, value in AnalyzeEnvelopes(self._portfolio).analyzeTime(self.current_date).items(): + + for key, value in AnalyzeEnvelopes(self._portfolio).analyze().items(): if key in self._log_enveloppe_values: self._log_enveloppe_values[key][-1] = value else: self._log_enveloppe_values[key] = [value] - for key, value in AnalyzeAssetClasses(self._portfolio).analyzeTime(self.current_date).items(): + + for key, value in AnalyzeAssetClasses(self._portfolio).analyze_flat().items(): self._log_assets_classes_values[key][-1] = value - for key, value in AnalyzeSubAssetClasses(self._portfolio).analyzeTime(self.current_date).items(): + + for key, value in AnalyzeAssetSubclasses(self._portfolio).analyze_flat().items(): if key in self._log_assets_subclasses_values: self._log_assets_subclasses_values[key][-1] = value else: self._log_assets_subclasses_values[key] = [value] - for key, value in AnalyzeLines(self._portfolio).analyzeTime(self.current_date).items(): + + for key, value in AnalyzeLines(self._portfolio).analyze().items(): if key in self._log_lines_values: self._log_lines_values[key][-1] = value else: self._log_lines_values[key] = [value] - def chartOnTimeline( + def chart_timeline( self, title: str, valuesToGraph: Dict[str, List[float]], @@ -249,7 +243,7 @@ def chartOnTimeline( "plotShadow": False, "type": "area", "zooming": {"type": "xy"}, - "height": 1200, + "height": 800, "width": 1000, }, "title": {"text": title, "align": "center"}, @@ -264,7 +258,7 @@ def chartOnTimeline( "series": [ { "name": key, - "data": self.convertDataSeries(value), + "data": self._convert_data_series(value), "visible": visible_by_default, "color": colors[key] if (key in colors) else {None}, } @@ -280,7 +274,7 @@ def chartOnTimeline( "credits": {"enabled": False}, } - def convertDataSeries(self, data: [float]) -> [Any]: + def _convert_data_series(self, data: List[float]) -> List[Any]: """Convert DataSeries in a time series format to allow non regular data""" res = [] i = 0