@@ -900,6 +900,22 @@ def cumulative_split_factor(ticker, from_date, to_date):
900900 # If we sold, we add the cost basis of those shares to the "closed bucket"
901901 positions [ticker ]['cost_of_closed' ] += cost_chunk
902902
903+ # First purchase and last sale dates per ticker (for annualized return)
904+ first_purchase_dates = {}
905+ last_sale_dates = {}
906+ for date , row in sorted_trades .iterrows ():
907+ t = row ['Ticker' ]
908+ qty_trade = row ['Quantity' ]
909+ if qty_trade > 0 : # Buy: use first purchase date
910+ if t not in first_purchase_dates or date < first_purchase_dates [t ]:
911+ first_purchase_dates [t ] = date
912+ elif qty_trade < 0 : # Sell: use last sale date
913+ if t not in last_sale_dates or date > last_sale_dates [t ]:
914+ last_sale_dates [t ] = date
915+ else :
916+ first_purchase_dates = {}
917+ last_sale_dates = {}
918+
903919 # 3. Process Dividends - No FX needed here!
904920 if self .dividend_income is not None and not self .dividend_income .empty :
905921 for date , row in self .dividend_income .iterrows ():
@@ -948,6 +964,22 @@ def cumulative_split_factor(ticker, from_date, to_date):
948964 # Math is identical regardless of currency
949965 return_pct = (total_return_native / roi_denominator * 100.0 ) if roi_denominator > 0 else 0.0
950966
967+ # Annualized return since purchase: (1 + Total Return %)^(365 / Days Held) - 1
968+ # Days Held: first purchase -> today (open) or last sale (closed); use first purchase only
969+ is_open = data ['qty' ] > 0.00001
970+ first_purchase = first_purchase_dates .get (ticker )
971+ if first_purchase is None or roi_denominator <= 0 :
972+ annualized_return_pct = float ('nan' )
973+ else :
974+ end_date = latest_date if is_open else last_sale_dates .get (ticker , first_purchase )
975+ start_d = pd .Timestamp (first_purchase )
976+ end_d = pd .Timestamp (end_date )
977+ days_held = (end_d - start_d ).days
978+ if days_held > 0 :
979+ annualized_return_pct = ((1.0 + return_pct / 100.0 ) ** (365.0 / days_held ) - 1.0 ) * 100.0
980+ else :
981+ annualized_return_pct = float ('nan' )
982+
951983 # --- Aggregation Prep (Normalized to CAD) ---
952984 # We calculate a hidden CAD market value solely for the weighting step
953985 fx_multiplier = latest_fx_usd_cad if currency == 'USD' else 1.0
@@ -971,6 +1003,7 @@ def cumulative_split_factor(ticker, from_date, to_date):
9711003 'total_return' : total_return_native ,
9721004 'total_return_cad_normalized' : total_return_native * latest_fx_usd_cad ,
9731005 'total_return_pct' : return_pct ,
1006+ 'annualized_return_pct' : annualized_return_pct ,
9741007
9751008 'mv_cad_normalized' : market_val_cad_calc ,
9761009 'invested_capital' : roi_denominator ,
0 commit comments