import pandas as pd
import numpy as np
import yfinance as yf
import matplotlib.pyplot as plt
import seaborn as sns
from datetime import datetime

sns.set_style("whitegrid")

class DCAStrategy:
    def __init__(self, ticker, start_date, end_date=None, initial_investment=10000, 
                 investment_frequency='ME', reinvest_dividends=True):
        self.ticker = ticker
        self.start_date = start_date
        self.end_date = end_date or datetime.now().strftime('%Y-%m-%d')
        self.initial_investment = initial_investment
        self.investment_frequency = investment_frequency
        self.reinvest_dividends = reinvest_dividends
        
        self.data = self._fetch_data()
        self.investment_dates = self._get_investment_dates()
        self.periodic_investment = self._calculate_periodic_investment()
    
    def _fetch_data(self):
        data = yf.download(self.ticker, start=self.start_date)
        if isinstance(data.columns, pd.MultiIndex):
            data.columns = data.columns.get_level_values(0)
        return data
    
    def _get_investment_dates(self):
        return pd.date_range(
            start=self.data.index[0],
            end=self.data.index[-1],
            freq=self.investment_frequency
        )
    
    def _calculate_periodic_investment(self):
        num_periods = len(self.investment_dates)
        return self.initial_investment / num_periods if num_periods > 0 else 0
    
    def _get_next_trading_day(self, date):
        next_dates = self.data.index[self.data.index >= date]
        return next_dates[0] if len(next_dates) > 0 else None

    def calculate_portfolio(self):
        # Initialize series with the full date range
        holdings = pd.Series(0.0, index=self.data.index)
        portfolio_value = pd.Series(0.0, index=self.data.index)
        total_invested = pd.Series(0.0, index=self.data.index)

        # Initial investment on first trading day
        first_trading_day = self.data.index[0]
        initial_price = self.data.loc[first_trading_day, 'Close']
        initial_shares = self.periodic_investment / initial_price
        
        holdings.iloc[0] = initial_shares
        total_invested.iloc[0] = self.periodic_investment

        # Process all investments including the first one
        for i, inv_date in enumerate(self.investment_dates[1:], 1):  # Start from second investment
            trading_date = self._get_next_trading_day(inv_date)
            if trading_date is not None:
                idx = self.data.index.get_loc(trading_date)
                price = self.data.loc[trading_date, 'Close']
                new_shares = self.periodic_investment / price
                
                # Update holdings and total invested
                holdings.iloc[idx:] = holdings.iloc[idx-1] + new_shares
                total_invested.iloc[idx:] = total_invested.iloc[idx-1] + self.periodic_investment

        # Handle dividends if enabled
        if self.reinvest_dividends and 'Dividends' in self.data.columns:
            dividends = self.data['Dividends']
            dividend_shares = (dividends * holdings) / self.data['Close']
            holdings = holdings + dividend_shares.fillna(0)

        # Calculate portfolio value
        portfolio_value = holdings * self.data['Close']
        
        # Forward fill any missing values
        holdings = holdings.ffill()
        total_invested = total_invested.ffill()
        portfolio_value = portfolio_value.ffill()

        # Store portfolio_value and total_invested as attributes
        self.portfolio_value = portfolio_value  
        self.total_invested = total_invested

        return portfolio_value, holdings, total_invested

    def analyze_performance(self):
        portfolio_value, holdings, total_invested = self.calculate_portfolio()
        
        if total_invested.iloc[-1] == 0:
            print("Warning: Total invested is zero. Check calculations.")
            return None
            
        final_value = portfolio_value.iloc[-1]
        total_return = (final_value - total_invested.iloc[-1]) / total_invested.iloc[-1]
        years = (self.data.index[-1] - self.data.index[0]).days / 365.25
        annualized_return = (1 + total_return) ** (1/years) - 1

        daily_returns = portfolio_value.pct_change()
        daily_returns = daily_returns.replace([np.inf, -np.inf], np.nan).dropna()

        volatility = daily_returns.std() * np.sqrt(252)
        risk_free_rate = 0.02
        sharpe_ratio = (annualized_return - risk_free_rate) / volatility

        return {
            'Initial Investment': self.initial_investment,
            'Final Value': final_value,
            'Total Return': total_return,
            'Annualized Return': annualized_return,
            'Volatility': volatility,
            'Sharpe Ratio': sharpe_ratio
        }

    def plot_performance(self):
        portfolio_value, holdings, total_invested = self.calculate_portfolio()

        # Validate data
        if portfolio_value.isna().any() or total_invested.isna().any():
            raise ValueError("Data contains NaN values. Check your calculations.")
        if len(portfolio_value) != len(total_invested):
            raise ValueError("Data lengths are mismatched. Ensure indices align.")

        # Plot portfolio value
        fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8))

        sns.lineplot(x=portfolio_value.index, y=portfolio_value.values, ax=ax1, linewidth=2, label='Portfolio Value')
        sns.lineplot(x=total_invested.index, y=total_invested.values, ax=ax1, linewidth=2, linestyle='--', color='red', label='Total Invested')

        ax1.set_title('Portfolio Value Over Time')
        ax1.set_xlabel('Date')
        ax1.set_ylabel('Portfolio Value ($)')
        ax1.legend()
        ax1.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f'${x:,.0f}'))
        ax1.set_ylim(bottom=self.initial_investment)  # Set y-axis limit to initial investment

        # Plot cumulative returns
        returns_percentage = (portfolio_value / self.initial_investment - 1) * 100
        sns.lineplot(x=returns_percentage.index, y=returns_percentage.values, ax=ax2, linewidth=2, label='Cumulative Returns')

        ax2.set_title('Cumulative Returns')
        ax2.set_xlabel('Date')
        ax2.set_ylabel('Return (%)')
        ax2.legend()
        ax2.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f'{x:.1f}%'))
        ax2.axhline(y=0, color='black', linestyle='-', alpha=0.2)

        ax2.set_ylim(bottom=-100)  # Set y-axis limit to -100%
        ax2.set_ylim(top=max(returns_percentage.max(), 0))  # Set y-axis limit to max return

        plt.tight_layout()
        plt.show()

# Example usage
strategy = DCAStrategy(
    ticker='^IBEX',
    start_date='2020-01-01',
    initial_investment=10000,
    investment_frequency='ME',
    reinvest_dividends=True
)

results = strategy.analyze_performance()
if results:
    print("\nPerformance Metrics:")
    print("-" * 50)
    for metric, value in results.items():
        if metric in ['Initial Investment', 'Final Value']:
            print(f"{metric}: ${value:,.2f}")
        else:
            print(f"{metric}: {value:.2%}")

strategy.plot_performance()
... improve thsi Nick Shawn strategy