| | import pandas as pd |
| | import numpy as np |
| | import random |
| |
|
| | |
| | eps = 1e-8 |
| | ANNUAL_TRADING_DAYS = 252 |
| |
|
| | def run_equal_weight(data_df: pd.DataFrame) -> pd.Series: |
| | """Calculates daily returns for a static equal-weight portfolio. |
| | |
| | Args: |
| | data_df (pd.DataFrame): DataFrame with dates as index, ticker returns, |
| | and an 'rf' column. |
| | |
| | Returns: |
| | pd.Series: Daily returns of the equal-weight portfolio. |
| | """ |
| | stock_returns = data_df.drop(columns=['rf'], errors='ignore') |
| | if stock_returns.empty: |
| | return pd.Series(dtype=float, name="EqualWeightReturn") |
| | |
| | daily_returns = stock_returns.mean(axis=1) |
| | return daily_returns.rename("EqualWeightReturn") |
| |
|
| | def run_random_portfolio( |
| | data_df: pd.DataFrame, |
| | num_stocks: int = 3, |
| | rebalance_days: int = 20 |
| | ) -> pd.Series: |
| | """Calculates daily returns for a randomly selected portfolio, |
| | rebalanced periodically. |
| | |
| | Args: |
| | data_df (pd.DataFrame): DataFrame with dates as index, ticker returns, |
| | and an 'rf' column. |
| | num_stocks (int): Number of stocks to randomly select. |
| | rebalance_days (int): How often to re-select stocks. |
| | |
| | Returns: |
| | pd.Series: Daily returns of the random portfolio. |
| | """ |
| | stock_returns = data_df.drop(columns=['rf'], errors='ignore') |
| | if stock_returns.empty or stock_returns.shape[1] < num_stocks: |
| | print("Warning: Not enough stocks available for random portfolio.") |
| | return pd.Series(dtype=float, name="RandomPortfolioReturn") |
| |
|
| | tickers = stock_returns.columns.tolist() |
| | portfolio_returns = pd.Series(index=data_df.index, dtype=float) |
| | selected_tickers = [] |
| |
|
| | for i, date in enumerate(data_df.index): |
| | |
| | if i % rebalance_days == 0 or not selected_tickers: |
| | if len(tickers) >= num_stocks: |
| | selected_tickers = random.sample(tickers, num_stocks) |
| | else: |
| | selected_tickers = tickers |
| | |
| |
|
| | |
| | daily_returns = stock_returns.loc[date, selected_tickers] |
| | portfolio_returns[date] = daily_returns.mean() |
| |
|
| | return portfolio_returns.rename("RandomPortfolioReturn") |
| |
|
| | |
| |
|
| | def calculate_cumulative_returns(returns_series: pd.Series) -> pd.Series: |
| | """Calculates cumulative returns from a daily returns series.""" |
| | return (1 + returns_series.fillna(0)).cumprod() |
| |
|
| | def calculate_performance_metrics(returns_series: pd.Series, rf_series: pd.Series) -> dict: |
| | """Calculates annualized Sharpe Ratio and Max Drawdown.""" |
| | if returns_series.empty or returns_series.isnull().all(): |
| | return {"Annualized Sharpe Ratio": 0.0, "Max Drawdown": 0.0, "Cumulative Return": 1.0} |
| | |
| | cumulative_return = (1 + returns_series.fillna(0)).cumprod().iloc[-1] |
| | |
| | |
| | aligned_rf = rf_series.reindex(returns_series.index).fillna(0) |
| | |
| | |
| | excess_returns = returns_series - aligned_rf |
| | |
| | |
| | |
| | mean_excess_return = excess_returns.mean() |
| | std_dev_excess_return = excess_returns.std() |
| | sharpe_ratio = (mean_excess_return / (std_dev_excess_return + eps)) * np.sqrt(ANNUAL_TRADING_DAYS) |
| | |
| | |
| | cumulative = calculate_cumulative_returns(returns_series) |
| | peak = cumulative.expanding(min_periods=1).max() |
| | drawdown = (cumulative - peak) / (peak + eps) |
| | max_drawdown = abs(drawdown.min()) |
| | |
| | return { |
| | "Annualized Sharpe Ratio": round(sharpe_ratio, 4), |
| | "Max Drawdown": round(max_drawdown, 4), |
| | "Cumulative Return": round(cumulative_return, 4) |
| | } |