22import os
33from datetime import date
44from typing import Any
5+ from typing import Dict
56from typing import List
67from typing import Optional
78from typing import Tuple
8- from typing import TYPE_CHECKING
99
1010import finalynx .theme
11- from docopt import docopt
11+ from docopt import docopt # type: ignore[import]
1212from finalynx import Dashboard
1313from finalynx import Fetch
1414from finalynx import Portfolio
2121from finalynx .portfolio .bucket import Bucket
2222from finalynx .portfolio .envelope import Envelope
2323from finalynx .portfolio .folder import Sidecar
24- from html2image import Html2Image
24+ from finalynx .simulator .timeline import Simulation
25+ from finalynx .simulator .timeline import Timeline
26+ from html2image import Html2Image # type: ignore[import]
2527from rich import inspect # noqa F401
2628from rich import pretty
2729from rich import print # noqa F401
2830from rich import traceback
2931from rich .columns import Columns
3032from rich .console import Console
3133from rich .panel import Panel
34+ from rich .prompt import Confirm
3235from rich .text import Text
3336from rich .tree import Tree
3437
35-
36- if TYPE_CHECKING :
37- from rich .console import ConsoleRenderable
38-
3938from .__meta__ import __version__
4039from .console import console
4140from .usage import __doc__
@@ -65,9 +64,11 @@ class Assistant:
6564
6665 def __init__ (
6766 self ,
67+ # Main structure elements
6868 portfolio : Portfolio ,
6969 buckets : Optional [List [Bucket ]] = None ,
7070 envelopes : Optional [List [Envelope ]] = None ,
71+ # Portfolio options
7172 ignore_orphans : bool = False ,
7273 clear_cache : bool = False ,
7374 force_signin : bool = False ,
@@ -81,15 +82,18 @@ def __init__(
8182 active_sources : Optional [List [str ]] = None ,
8283 theme : Optional [finalynx .theme .Theme ] = None ,
8384 sidecars : Optional [List [Sidecar ]] = None ,
85+ ignore_argv : bool = False ,
86+ # Budget options
8487 check_budget : bool = False ,
8588 interactive : bool = False ,
86- ignore_argv : bool = False ,
89+ # Simulation options
90+ simulation : Optional [Simulation ] = None ,
8791 ):
8892 self .portfolio = portfolio
8993 self .buckets = buckets if buckets else []
9094 self .envelopes = envelopes if envelopes else []
9195
92- # Options that can either be set in the constructor or from the command line options, type --help
96+ # Options that can either be set in the constructor or from the command line options, see --help
9397 self .ignore_orphans = ignore_orphans
9498 self .clear_cache = clear_cache
9599 self .force_signin = force_signin
@@ -104,6 +108,7 @@ def __init__(
104108 self .sidecars = sidecars if sidecars else []
105109 self .check_budget = check_budget
106110 self .interactive = interactive
111+ self .simulation = simulation
107112
108113 # Set the global color theme if specified
109114 if theme :
@@ -117,6 +122,9 @@ def __init__(
117122 self ._fetch = Fetch (self .portfolio , self .clear_cache , self .ignore_orphans )
118123 self .budget = Budget ()
119124
125+ # Initialize the simulation timeline with the initial user events
126+ self ._timeline = Timeline (simulation , self .portfolio , self .buckets ) if simulation else None
127+
120128 def add_source (self , source : SourceBaseLine ) -> None :
121129 """Register a source, either defined in your own config or from the available Finalynx sources
122130 using `from finalynx.fetch.source_any import SourceAny`."""
@@ -204,8 +212,8 @@ def run(self) -> None:
204212
205213 # Render the console elements
206214 main_frame = self .render_mainframe ()
207- panels = self .render_panels ()
208- renders : List [Any ] = [main_frame , panels ]
215+ dict_panels , render_panels = self .render_panels ()
216+ renders : List [Any ] = [main_frame , render_panels ]
209217 if self .check_budget :
210218 renders .append (self .budget .render_expenses ())
211219
@@ -217,9 +225,32 @@ def run(self) -> None:
217225 if self .show_data :
218226 console .print (Panel (fetched_tree , title = "Fetched data" ))
219227
228+ # Run the simulation if there are events defined
229+ if self ._timeline and not self ._timeline .is_finished :
230+ console .log (f"Running simulation until { self ._timeline .end_date } ..." )
231+ tree = Tree ("\n [bold]Worth" , guide_style = TH ().TREE_BRANCH )
232+
233+ def append_worth (year : int , amount : float ) -> None :
234+ tree .add (f"[{ TH ().TEXT } ]{ year } : [{ TH ().ACCENT } ][bold]{ round (amount / 1000 ):>4} [/] k€" )
235+
236+ append_worth (date .today ().year , self .portfolio .get_amount ())
237+ for year in range (date .today ().year + 5 , self ._timeline .end_date .year , 5 ):
238+ self ._timeline .goto (date (year , 1 , 1 ))
239+ append_worth (year , self .portfolio .get_amount ())
240+ self ._timeline .run ()
241+ append_worth (self ._timeline .current_date .year , self .portfolio .get_amount ())
242+ dict_panels ["performance" ].add (tree )
243+
244+ console .log (f" Portfolio will be worth [{ TH ().ACCENT } ]{ self .portfolio .get_amount ():.0f} €[/]" )
245+
220246 # Display the entire portfolio and associated recommendations
221247 for render in renders :
222248 console .print ("\n \n " , render )
249+ console .print ("\n " )
250+
251+ # TODO replace with a command option
252+ if self ._timeline and Confirm .ask (f"Display your future portfolio in { self ._timeline .end_date } ?" ):
253+ console .print ("\n \n " , self .render_mainframe ())
223254
224255 # Interactive review of the budget expenses if enabled
225256 if self .check_budget and self .interactive :
@@ -267,33 +298,37 @@ def render_mainframe(self) -> Columns:
267298
268299 return Columns (main_frame , padding = (0 , 0 )) # type: ignore
269300
270- def render_panels (self ) -> Columns :
301+ def render_panels (self ) -> Tuple [ Dict [ str , Any ], Columns ] :
271302 """Renders the default set of panels used in the default console view when calling run()."""
272303
273304 def panel (title : str , content : Any ) -> Panel :
274305 return Panel (content , title = title , padding = (1 , 2 ), expand = False , border_style = TH ().PANEL )
275306
276- # Final set of results to be displayed
277- panels : List [ConsoleRenderable ] = [
278- Text (" " ),
279- panel ("Recommendations" , render_recommendations (self .portfolio , self .envelopes )),
280- panel ("Performance" , self .render_performance_report ()),
281- ]
307+ dict_items : Dict [str , Any ] = {
308+ "recommendations" : render_recommendations (self .portfolio , self .envelopes ),
309+ "performance" : self .render_performance_report (),
310+ }
282311
283312 # Add the budget panel if enabled
284313 if self .check_budget :
285- panels . append ( panel ( "Budget" , self .budget .render_summary ()) )
314+ dict_items [ "budget" ] = self .budget .render_summary ()
286315
287- return Columns (panels , padding = (2 , 2 ))
316+ return dict_items , Columns (
317+ [Text (" " )] + [panel (k .capitalize (), v ) for k , v in dict_items .items ()], padding = (2 , 2 )
318+ )
288319
289320 def render_performance_report (self ) -> Tree :
290321 """Print the current and ideal global expected performance. Call either run() or initialize() first."""
291322 perf = self .portfolio .get_perf (ideal = False ).expected
292323 perf_ideal = self .portfolio .get_perf (ideal = True ).expected
293324
294- tree = Tree ("Global Performance" , hide_root = True )
295- tree .add (f"[{ TH ().TEXT } ]Current: [bold][{ TH ().ACCENT } ]{ perf :.1f} %[/] / year" )
296- tree .add (f"[{ TH ().TEXT } ]Planned: [bold][{ TH ().ACCENT } ]{ perf_ideal :.1f} %[/] / year" )
325+ tree = Tree ("Global Performance" , hide_root = True , guide_style = TH ().TREE_BRANCH )
326+ node = tree .add ("[bold]Yield" )
327+ node .add (f"[{ TH ().TEXT } ]Current: [bold][{ TH ().ACCENT } ]{ perf :.2f} %[/] / year" )
328+ node .add (f"[{ TH ().TEXT } ]Planned: [bold][{ TH ().ACCENT } ]{ perf_ideal :.2f} %[/] / year" )
329+
330+ if self .simulation :
331+ node .add (f"[{ TH ().TEXT } ]Inflation: [bold][gold1]{ self .simulation .inflation :.2f} %[/] / year" )
297332 return tree
298333
299334 def dashboard (self ) -> None :
@@ -351,7 +386,7 @@ def export_img(
351386 # Export the entire portfolio tree to HTML and set the zoom
352387 dashboard_console = Console (record = True , file = open (os .devnull , "w" ))
353388 dashboard_console .print (self .render_mainframe ())
354- dashboard_console .print (self .render_panels ())
389+ dashboard_console .print (self .render_panels ()[ 1 ] )
355390 output_html = dashboard_console .export_html ().replace ("body {" , f"body {{\n zoom: { zoom } ;" )
356391
357392 # Convert the HTML to PNG
0 commit comments