Domain: Financial & Accounting

**Pharmaceutical Portfolio Analysis: Return and Risk Assessment**

You are required to conduct a quantitative performance and risk evaluation of a pharmaceutical sector equity portfolio comprising four Indian pharmaceutical companies: Dr. Reddy's Laboratories Limited, Divi's Laboratories Limited, Torrent Pharmaceuticals Limited, and Lupin Limited. Historical closing price data for these four securities is provided for your analysis.

**Data Specifications:**
Utilize only the closing prices of all four stocks for all calculations. Assume 252 trading days per annum for annualization purposes.

**Required Analytical Tasks:**

1. **Calculate the annualized returns for each of the four pharmaceutical stocks individually:**
   - Compute daily logarithmic returns as: r_t = ln(P_t / P_{t-1})
   - Calculate the mean daily logarithmic return
   - Annualize using the formula: Annual Return = exp(mean_daily_log_return × 252) - 1
   - Convert to percentage and round to 6 decimal places

2. **Identify and report which stock generates the maximum annualized return** among the four securities

3. **Compute the annualized standard deviation (volatility) for each of the four stocks:**
   - Calculate the sample standard deviation of daily logarithmic returns (using N-1 degrees of freedom)
   - Annualize using the formula: Annual StdDev = std_daily_log_return × √252
   - Convert to percentage and round to 6 decimal places

4. **Calculate the Sharpe ratio for each stock:**
   - Use risk-free rate of 5.25% (0.0525)
   - Formula: Sharpe Ratio = (Annual Return - Risk-Free Rate) / Annual StdDev
   - Round to 6 decimal places

5. **Compute the annual standard deviation of an equally weighted portfolio:**
   - Construct a portfolio with 25% weight allocated to each of the four stocks
   - Compute the daily returns of the portfolio as the weighted average of individual stock daily logarithmic returns
   - Calculate the sample standard deviation of the portfolio's daily returns (using N-1 degrees of freedom)
   - Annualize using the formula: Portfolio Annual StdDev = std_daily_portfolio_return × √252
   - Convert to percentage and round to 6 decimal places

**Output Format Requirements:**
- All percentage values must be expressed as percentages with the % symbol
- All numerical values must be rounded to exactly 6 decimal places
- Annual returns: express as percentage (e.g., 24.305300%)
- Annual standard deviations: express as percentage (e.g., 21.403200%)
- Sharpe ratios: express as decimal number (e.g., 0.890299)

**Deliverables:**
Present your findings in a structured format displaying:
- Annual return for each stock (as percentage with 6 decimal places)
- Annual standard deviation for each stock (as percentage with 6 decimal places)
- Sharpe ratio for each stock (as decimal with 6 decimal places)
- Identification of the highest-performing stock by name
- Annual standard deviation of the equally weighted portfolio (as percentage with 6 decimal places)

Ensure methodological rigor in deriving daily returns, annualizing metrics appropriately, and maintaining computational accuracy throughout the analysis.

Grading rubric:
criterion: The annualized return of Dr Reddy is correctly reported as 1.164291%
weight: 5
criterion: The annualized return of Lupin is correctly reported as -0.047550%
weight: 5
criterion: The annualized return of Torrent Pharma is correctly reported as 24.305269%
weight: 5
criterion: The annualized return of Divis Lab is correctly reported as 1.472161%
weight: 5
criterion: The annualized standard deviation of Dr Reddy is correctly reported as 21.332902%
weight: 5
criterion: The annualized standard deviation of Lupin is correctly reported as 24.884945%
weight: 5
criterion: The annualized standard deviation of Torrent Pharma is correctly reported as 21.403227%
weight: 5
criterion: The annualized standard deviation of Divis Lab is correctly reported as 25.851727%
weight: 5
criterion: The stock with the highest annualized return is correctly identified as Torrent Pharma
weight: 10
criterion: The Sharpe ratio of Dr Reddy is correctly reported as -0.191521
weight: 5
criterion: The Sharpe ratio of Lupin is correctly reported as -0.212882
weight: 5
criterion: The Sharpe ratio of Torrent Pharma is correctly reported as 0.890299
weight: 10
criterion: The Sharpe ratio of Divis Lab is correctly reported as -0.146135
weight: 5
criterion: The annualized standard deviation of the equally weighted portfolio is correctly reported as 17.053272%
weight: 10

Golden answer:
```python
from pathlib import Path
import pandas as pd
import numpy as np

TRADING_DAYS = 252
RISK_FREE_RATE = 0.0525  # 5.25%

# Update paths to uploaded files location
FILES = {
    "Dr Reddy": "Quote-Equity-DRREDDY-EQ-09-02-2025-09-02-2026.csv",
    "Divis Lab": "Quote-Equity-DIVISLAB-EQ-09-02-2025-09-02-2026.csv",
    "Torrent Pharma": "Quote-Equity-TORNTPHARM-EQ-09-02-2025-09-02-2026.csv",
    "Lupin": "Quote-Equity-LUPIN-EQ-09-02-2025-09-02-2026.csv"
}


def _find_close_column(df: pd.DataFrame) -> str:
    """Return the name of the 'close' column (case-insensitive). Raises KeyError if not found."""
    for col in df.columns:
        if col.strip().lower() in ("close", "close*", "ltp"):
            return col
    raise KeyError(f"No 'Close' column found. Available columns: {list(df.columns)}")


def load_close_series(path: str) -> pd.Series:
    """
    Load CSV, sort by date (if present), extract and clean CLOSE column, return numeric Series.
    """
    p = Path(path)
    if not p.exists():
        raise FileNotFoundError(f"File not found: {path}")

    df = pd.read_csv(p, dtype=str)

    # Try to parse DATE if present and sort chronologically (ascending)
    date_col = None
    for c in df.columns:
        if c.strip().lower() in ("date", "trade date", "timestamp"):
            date_col = c
            break
    if date_col is not None:
        try:
            df[date_col] = pd.to_datetime(df[date_col], dayfirst=True, errors="coerce")
            df = df.sort_values(date_col).reset_index(drop=True)
        except Exception:
            pass

    close_col = _find_close_column(df)

    # Clean numeric text
    series = (
        df[close_col]
        .astype(str)
        .str.replace(",", "", regex=False)
        .str.replace(" ", "", regex=False)
        .str.replace("—", "", regex=False)
        .str.replace("-", "", regex=False)
    )

    close_numeric = pd.to_numeric(series, errors="coerce").dropna()

    if len(close_numeric) < 2:
        raise ValueError(f"Not enough valid CLOSE observations in {path} (need >=2).")

    return close_numeric.reset_index(drop=True)


def compute_metrics_from_close(close_prices: pd.Series):
    """
    Given closing prices in chronological order, compute:
      - annual_return = exp(mean_daily_log * 252) - 1
      - annual_std = std_daily_log * sqrt(252)   (sample std, ddof=1)
      - sharpe = (annual_return - Rf) / annual_std
    Returns values as floats (unrounded).
    """
    log_rets = np.log(close_prices / close_prices.shift(1)).dropna()

    mean_daily_log = float(log_rets.mean())
    std_daily_log = float(log_rets.std(ddof=1))

    annual_return = float(np.exp(mean_daily_log * TRADING_DAYS) - 1.0)
    annual_std = float(std_daily_log * np.sqrt(TRADING_DAYS))

    if annual_std == 0:
        sharpe = float("nan")
    else:
        sharpe = float((annual_return - RISK_FREE_RATE) / annual_std)

    return log_rets, annual_return, annual_std, sharpe


def format_percentage(value, decimals=6):
    """Format a decimal value as percentage with specified decimal places."""
    return f"{round(value * 100, decimals):.{decimals}f}%"


def format_decimal(value, decimals=6):
    """Format a decimal value with specified decimal places."""
    return f"{round(value, decimals):.{decimals}f}"


def analyze_portfolio(files_map: dict):
    """
    Compute metrics for each stock and portfolio.
    Returns:
      - Dictionary with formatted results for each stock
      - Highest return stock name
      - Portfolio annual standard deviation (formatted)
    """
    stock_data = {}
    stock_results = {}
    
    for name, filepath in files_map.items():
        try:
            closes = load_close_series(filepath)
            log_rets, ann_ret, ann_std, sharpe = compute_metrics_from_close(closes)
            
            stock_results[name] = {
                "Annual Return": format_percentage(ann_ret),
                "Annual StdDev": format_percentage(ann_std),
                "Sharpe Ratio": format_decimal(sharpe)
            }
            
            # Store log returns and raw values for portfolio calculation
            stock_data[name] = {
                "log_returns": log_rets,
                "annual_return": ann_ret
            }
            
        except Exception as e:
            stock_results[name] = {
                "Error": str(e)
            }

    # Find stock with maximum return
    max_stock = None
    max_return = -float('inf')
    for name, data in stock_data.items():
        if data["annual_return"] > max_return:
            max_return = data["annual_return"]
            max_stock = name
    
    # Compute equally weighted portfolio standard deviation
    portfolio_std_formatted = None
    if len(stock_data) == 4:
        try:
            # Align all log return series
            all_returns_dict = {name: data["log_returns"] for name, data in stock_data.items()}
            all_returns = pd.DataFrame(all_returns_dict)
            # Equal weights: 25% each
            portfolio_daily_returns = all_returns.mean(axis=1)
            # Sample standard deviation
            portfolio_daily_std = portfolio_daily_returns.std(ddof=1)
            # Annualize
            portfolio_annual_std = float(portfolio_daily_std * np.sqrt(TRADING_DAYS))
            portfolio_std_formatted = format_percentage(portfolio_annual_std)
        except Exception as e:
            portfolio_std_formatted = f"Error: {e}"
    
    return stock_results, max_stock, portfolio_std_formatted


def main():
    stock_results, max_stock, portfolio_std = analyze_portfolio(FILES)

    print("\n" + "="*80)
    print("PHARMACEUTICAL PORTFOLIO ANALYSIS RESULTS")
    print("="*80 + "\n")

    print("Individual Stock Metrics:")
    print("-" * 80)
    for stock_name, metrics in stock_results.items():
        print(f"\n{stock_name}:")
        for metric_name, value in metrics.items():
            print(f"  {metric_name}: {value}")
    
    print("\n" + "-" * 80)
    print(f"\nStock with Maximum Annual Return: {max_stock}")
    print(f"\nEqually Weighted Portfolio Annual Standard Deviation: {portfolio_std}")
    print("\n" + "="*80 + "\n")


if __name__ == "__main__":
    main()
```

**Explanation:**

```
### **Step-by-Step Explanation:**

**Step 1: Load Data and Extract Closing Prices**
- Read the CSV files for all four stocks
- Extract the closing price column from each file
- Sort data chronologically (earliest to latest)

**Step 2: Calculate Daily Logarithmic Returns**
For each stock, compute:
- r_t = ln(P_t / P_{t-1}) for each consecutive pair of prices
- This produces a series of daily log returns

**Step 3: Compute Annualized Returns**
For each stock:
- Calculate mean of daily log returns: mean_daily_log
- Apply annualization formula: Annual Return = exp(mean_daily_log × 252) - 1
- Convert to percentage: multiply by 100
- Round to 6 decimal places
- Format with % symbol

**Step 4: Compute Annualized Standard Deviation (Volatility)**
For each stock:
- Calculate sample standard deviation of daily log returns: std_daily_log (using N-1 degrees of freedom)
- Annualize: Annual StdDev = std_daily_log × √252
- Convert to percentage: multiply by 100
- Round to 6 decimal places
- Format with % symbol

**Step 5: Calculate Sharpe Ratios**
For each stock:
- Sharpe Ratio = (Annual Return - 0.0525) / Annual StdDev
- Note: Use the decimal form of returns for this calculation
- Round to 6 decimal places

**Step 6: Identify Maximum Return Stock**
- Compare annualized returns across all four stocks
- Identify the stock with the highest value
- Report the stock name

**Step 7: Compute Portfolio Annual Standard Deviation**
- Assign 25% weight to each stock
- For each day, calculate portfolio return as: 0.25 × r_DrReddy + 0.25 × r_Divis + 0.25 × r_Torrent + 0.25 × r_Lupin
- Calculate sample standard deviation of these daily portfolio returns (using N-1 degrees of freedom)
- Annualize: Portfolio Annual StdDev = std_daily_portfolio × √252
- Convert to percentage: multiply by 100
- Round to 6 decimal places
- Format with % symbol

**Step 8: Format and Report**
All outputs formatted according to specification with exactly 6 decimal places.
```

**GTFA:**

```
================================================================================
PHARMACEUTICAL PORTFOLIO ANALYSIS RESULTS
================================================================================

Individual Stock Metrics:
--------------------------------------------------------------------------------

Dr Reddy:
  Annual Return: 1.164291%
  Annual StdDev: 21.332902%
  Sharpe Ratio: -0.191521

Divis Lab:
  Annual Return: 1.472161%
  Annual StdDev: 25.851727%
  Sharpe Ratio: -0.146135

Torrent Pharma:
  Annual Return: 24.305269%
  Annual StdDev: 21.403227%
  Sharpe Ratio: 0.890299

Lupin:
  Annual Return: -0.047550%
  Annual StdDev: 24.884945%
  Sharpe Ratio: -0.212882

--------------------------------------------------------------------------------

Stock with Maximum Annual Return: Torrent Pharma

Equally Weighted Portfolio Annual Standard Deviation: 17.053272%

================================================================================
```