Untitled
unknown
plain_text
9 months ago
13 kB
7
Indexable
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# PARAMETERS
LOOKBACK_WINDOWS = [60, 120, 240] # approximate 3, 6, 12 months (for daily data)
TOP_N = 5 # number of risk-on assets to select at each rebalance
# Load the historic prices data
# Assumes the CSV has dates in the first column (which we'll parse as index)
prices = pd.read_csv('historic_prices.csv', index_col=0, parse_dates=True)
prices.sort_index(inplace=True)
# Calculate daily returns for later use (for risk-adjusted momentum denominator)
daily_log_returns = np.log(prices / prices.shift(1))
# Create dictionaries to hold each momentum measure for each window
tr_mom = {} # Total Return Momentum
pma_mom = {} # Price minus Moving Average
ram_mom = {} # Risk Adjusted Momentum
for window in LOOKBACK_WINDOWS:
# Total return momentum: (P_t - P_{t-window}) / P_{t-window}
tr_mom[window] = (prices - prices.shift(window)) / prices.shift(window)
# Price minus Moving Average: (P_t / SMA(P, n)) - 1
sma = prices.rolling(window=window).mean()
pma_mom[window] = (prices / sma) - 1
# Risk-adjusted momentum:
# Numerator: ln(P_t / P_{t-window})
num = np.log(prices / prices.shift(window))
# Denom: Rolling sum of absolute daily log returns over the window
den = daily_log_returns.abs().rolling(window=window).sum()
ram_mom[window] = num / den
# We now have 3 sets of signals each with 3 windows (9 signals total)
# For each rebalancing date, we will compute the composite score for each asset by summing its ranks
# Determine monthly rebalancing dates (we use month end dates)
rebalance_dates = prices.resample('M').last().index
# Prepare containers for storing portfolio weights and performance
weights_list = [] # each element is a DataFrame of asset weights at a given rebalance date
portfolio_returns = pd.Series(dtype=float)
aum = [] # cumulative AUM at each rebalance period
# Starting AUM
initial_aum = 100.0
current_aum = initial_aum
# For each rebalance date, compute composite momentum scores from available signals
for i, reb_date in enumerate(rebalance_dates):
# Ensure that we have enough data for the longest lookback window
if reb_date < prices.index[LOOKBACK_WINDOWS[-1]]:
continue
# Extract the prices as of the rebalance date (we assume signals are computed up to and including that day)
# We need to get the signal values for each asset at that date from each measure/window.
composite_score = pd.Series(0, index=prices.columns, dtype=float)
positive_signals_count = pd.Series(0, index=prices.columns, dtype=int)
# For each of the 9 signals, compute the cross-sectional rank (higher is better).
# Here we use ranking on the value (using the 'average' method).
for window in LOOKBACK_WINDOWS:
for signal_dict in [tr_mom, pma_mom, ram_mom]:
# Get the signal values on the rebalance date; drop NaNs
signal_vals = signal_dict[window].loc[reb_date]
# Drop assets with NaN (due to insufficient history)
valid_signal = signal_vals.dropna()
if valid_signal.empty:
continue
# Rank: highest value gets the highest rank
ranks = valid_signal.rank(method='average', ascending=True)
# Note: we want higher momentum to have higher scores. One option is to simply use the rank number.
# Scale rank to have mean zero (optional) or just sum them.
composite_score[valid_signal.index] += ranks
# Count how many signals are positive
positive_signals_count[valid_signal.index] += (valid_signal > 0).astype(int)
# Determine an absolute momentum filter: e.g. require at least 5 out of 9 signals to be positive.
abs_threshold = 5
qualified = positive_signals_count >= abs_threshold
# Keep only qualified assets
composite_score = composite_score[qualified]
# If no asset qualifies, go risk-off (empty weights, i.e. 100% risk-off asset)
if composite_score.empty:
selected_assets = []
else:
# Select the top TOP_N assets based on composite score (highest sum of ranks)
selected_assets = composite_score.sort_values(ascending=False).head(TOP_N).index.tolist()
# Define weights: if no asset qualifies, we allocate to risk-off asset (we assume the risk-off asset is the one with ticker 'SHV' or similar)
# You can adjust this to match your risk-off universe.
if len(selected_assets) == 0:
allocation = pd.Series(0, index=prices.columns)
if 'SHV' in prices.columns:
allocation['SHV'] = 1.0
else:
# if no specific risk-off asset is provided, stay in cash (represented by NaN or a cash series)
allocation['CASH'] = 1.0
else:
# Equal weighting for selected assets
allocation = pd.Series(0, index=prices.columns, dtype=float)
allocation[selected_assets] = 1.0 / len(selected_assets)
allocation.name = reb_date
weights_list.append(allocation)
# Determine the next rebalancing date (or use the end of the series)
if i < len(rebalance_dates) - 1:
next_reb_date = rebalance_dates[i+1]
else:
next_reb_date = prices.index[-1]
# Calculate returns from current rebalance date to next rebalancing date (using daily returns)
period_prices = prices.loc[reb_date:next_reb_date]
# Compute daily portfolio returns: weighted sum of asset returns.
# First, compute daily returns for the period
period_daily_returns = period_prices.pct_change().fillna(0)
# Multiply each column by its weight (broadcasting)
portfolio_daily_returns = (period_daily_returns * allocation).sum(axis=1)
# Compound returns for the period (excluding the first day which is rebalance day)
period_return = (1 + portfolio_daily_returns).cumprod().iloc[-1] - 1
# Update AUM
current_aum = current_aum * (1 + period_return)
aum.append(current_aum)
# Save the period return (using the next rebalancing date as the label)
portfolio_returns[next_reb_date] = period_return
# Combine weights into a DataFrame for inspection
weights_df = pd.DataFrame(weights_list)
weights_df.index.name = 'Rebalance_Date'
# Create a DataFrame for portfolio performance summary
performance_df = pd.DataFrame({
'Period_Return': portfolio_returns,
'AUM': pd.Series(aum, index=portfolio_returns.index)
})
# Print outputs
print("=== Portfolio Weights at Rebalance Dates ===")
print(weights_df)
print("\n=== Portfolio Period Returns ===")
print(portfolio_returns)
print("\n=== Portfolio AUM Over Time ===")
print(performance_df)
# Optionally, plot the cumulative AUM
plt.figure(figsize=(10, 5))
plt.plot(performance_df.index, performance_df['AUM'], marker='o')
plt.xlabel('Date')
plt.ylabel('Portfolio AUM')
plt.title('Cumulative Portfolio AUM')
plt.grid(True)
plt.show()
import matplotlib.pyplot as plt
# -------------------------------
# 4. Performance Metrics Calculation
# -------------------------------
# Load the risk free rate data
risk_free_filepath = r"\\asiapac.nom\data\MUM\IWM\India_IWM_IPAS\Reet\Momentum Strategy\Codes\Risk Free Rate.xlsx"
risk_free_df = pd.read_excel(risk_free_filepath, sheet_name="Risk Free Rate", parse_dates=[0])
risk_free_df.columns = ['Date', '1m', '3m']
def calculate_sharpe_ratio(returns, period):
volatility = returns.std()
avg_return = returns.mean()
# Map rebalance period to an annualization factor
annualization_factor = {1: 12, 2: 6, 3: 4}.get(period, 12) # Default to 12 if not found
return (avg_return / volatility) * (annualization_factor ** 0.5) if volatility > 0 else np.nan
# Ensure that there is a 'Date' column (if not, reset the index)
if 'Date' not in result_df.columns:
result_df = result_df.reset_index().rename(columns={'index': 'Date'})
# Merge risk free rate data on the 'Date' column
merged_df = pd.merge(result_df, risk_free_df, on='Date', how='left')
# Adjust risk-free rate based on the rebalance period
if rebalance_period == 1:
merged_df['rf_adjust'] = merged_df['1m'] / 12
elif rebalance_period == 2:
merged_df['rf_adjust'] = merged_df['1m'] / 6
elif rebalance_period == 3:
merged_df['rf_adjust'] = merged_df['1m'] / 4
else:
merged_df['rf_adjust'] = 0
# Compute excess returns by subtracting the risk free rate (adjusted to monthly) from monthly returns
merged_df['Excess Return'] = merged_df['Return'] - (merged_df['rf_adjust'] / 100)
# Calculate performance metrics using excess returns
excess_returns = merged_df['Excess Return'].dropna()
sharpe_ratio = calculate_sharpe_ratio(excess_returns, rebalance_period)
annualization_factor = {1: 12, 2: 6, 3: 4}.get(rebalance_period, 12)
avg_monthly_excess_return = excess_returns.mean()
monthly_excess_volatility = excess_returns.std()
annualized_volatility = monthly_excess_volatility * (annualization_factor ** 0.5)
annualised_risk_premium = avg_monthly_excess_return * annualization_factor
# Calculate cumulative return and annualized return
initial_aum = result_df['Portfolio Value'].iloc[0]
final_aum = result_df['Portfolio Value'].iloc[-1]
cumulative_return = (final_aum / initial_aum) - 1
# Convert start_date and end_date to datetime objects if necessary
from datetime import datetime
if isinstance(start_date, str):
start_date = datetime.strptime(start_date, '%Y-%m-%d')
if isinstance(end_date, str):
end_date = datetime.strptime(end_date, '%Y-%m-%d')
num_months = (end_date.year - start_date.year) * 12 + (end_date.month - start_date.month)
annualized_return = ((1 + cumulative_return) ** (12 / num_months)) - 1
def compute_max_drawdown(returns):
cumulative = (1 + returns).cumprod()
running_max = cumulative.cummax()
drawdown = (cumulative - running_max) / running_max
return drawdown.min()
max_drawdown = compute_max_drawdown(result_df['Return'])
# Calculate annual returns
annual_returns = result_df.groupby(result_df['Date'].dt.year)['Return'].apply(lambda x: (1 + x).prod() - 1)
min_annual_return = annual_returns.min()
max_annual_return = annual_returns.max()
# Create a DataFrame with all metrics
metrics_df = pd.DataFrame({
'Metric': [
'Cumulative Return',
'Annualized Return',
'Average Monthly Excess Return',
'Monthly Excess Volatility',
'Annualized Volatility',
'Sharpe Ratio',
'Annualized Risk Premium',
'Maximum Drawdown',
'Minimum Annual Return',
'Maximum Annual Return'
],
'Value': [
cumulative_return,
annualized_return,
avg_monthly_excess_return,
monthly_excess_volatility,
annualized_volatility,
sharpe_ratio,
annualised_risk_premium,
max_drawdown,
min_annual_return,
max_annual_return
]
})
# Format the values as percentages where appropriate
percentage_metrics = ['Cumulative Return', 'Annualized Return', 'Average Monthly Excess Return',
'Monthly Excess Volatility', 'Annualized Volatility', 'Maximum Drawdown',
'Minimum Annual Return', 'Maximum Annual Return', 'Annualized Risk Premium']
for metric in percentage_metrics:
metrics_df.loc[metrics_df['Metric'] == metric, 'Value'] = metrics_df.loc[metrics_df['Metric'] == metric, 'Value'].apply(lambda x: f"{x:.2%}")
# Format Sharpe Ratio to 2 decimal places
metrics_df.loc[metrics_df['Metric'] == 'Sharpe Ratio', 'Value'] = metrics_df.loc[metrics_df['Metric'] == 'Sharpe Ratio', 'Value'].apply(lambda x: f"{float(x):.2f}")
print("\nPerformance Metrics for the Portfolio:")
print(metrics_df.to_string(index=False))
# -------------------------------
# 5. Plotting the Weights Evolution
# -------------------------------
# Provided color palette for plotting (one color per asset)
colors = ['#CA2420', '#737373', '#80A9AE', '#00305C', '#80003F', '#CC8D19',
'#AEADB0', '#00713B', '#DCB940', '#00677A', '#9A9500', '#8F3336',
'#5A7400', '#B0CAD0', '#6077A3', '#995E7A', '#DCB172', '#D4D4D4',
'#6B9977', '#E5CC89', '#649CB3', '#B7B56B', '#BF8B79', '#8F9D66', '#000000']
# Plot evolution of asset weights over rebalancing dates from weights_df
plt.figure(figsize=(12, 6))
for i, ticker in enumerate(weights_df.columns):
# Use modulo to cycle through colors if there are more tickers than colors provided.
plt.plot(weights_df.index, weights_df[ticker], label=ticker, color=colors[i % len(colors)], marker='o')
plt.xlabel('Rebalance Date')
plt.ylabel('Asset Weight')
plt.title('Evolution of Asset Weights Over Time')
plt.legend(loc='upper left', bbox_to_anchor=(1,1))
plt.tight_layout()
plt.show()
Editor is loading...
Leave a Comment