Untitled

 avatar
unknown
plain_text
13 days ago
13 kB
2
Indexable
import pandas as pd
import numpy as np
from datetime import datetime
from dateutil.relativedelta import relativedelta

###############################################
# 1. Data Loading
###############################################

def load_price_data(filepath):
    """
    Load historical prices from an Excel file.
    Assumes that the first column is dates and the remaining columns are tickers.
    """
    df = pd.read_excel(filepath, index_col=0)
    df.index = pd.to_datetime(df.index)
    return df

def load_macro_indics(filepath):
    """
    Load macro indicators from an Excel file.
    Assumes that the first column is dates and the next three columns are:
      - VIX
      - Credit Spread
      - Consumer Confidence
    """
    df = pd.read_excel(filepath, index_col=0)
    df.index = pd.to_datetime(df.index)
    # Rename columns for clarity; adjust if your file already has header names.
    df.columns = ['VIX', 'CreditSpread', 'ConsumerConfidence']
    return df

###############################################
# 2. Helper: Observation Dates
###############################################

def get_observation_dates(start_date, end_date, rebalance_period):
    """
    Returns a list of observation dates from start_date to end_date
    with a step equal to rebalance_period (in months).
    """
    dates = []
    current = start_date
    while current <= end_date:
        dates.append(current)
        current += relativedelta(months=rebalance_period)
    return dates

###############################################
# 3. Initialize the Portfolio (with Cash)
###############################################

def initialize_portfolio(prices, date, tickers, initial_aum):
    """
    On the start date, invest equal notional amounts in each asset.
    Returns a dictionary mapping ticker -> quantity and includes a 'cash' key.
    Initially, we are fully invested so cash is 0.
    """
    portfolio = {}
    allocation = initial_aum / len(tickers)
    for ticker in tickers:
        price = prices.loc[date, ticker]
        portfolio[ticker] = allocation / price
    portfolio["cash"] = 0.0
    return portfolio

###############################################
# 4. Lookback Metric Computation
###############################################

def compute_lookback_metric(prices, current_date, ticker, lookback_period, metric_type='simple'):
    """
    Computes the lookback metric for one ticker.
    """
    prices = prices.sort_index()
    lookback_date = current_date - relativedelta(months=lookback_period)
    current_price = prices[ticker].asof(current_date)
    lookback_price = prices[ticker].asof(lookback_date)
    if pd.isna(current_price) or pd.isna(lookback_price):
        raise ValueError(f"Missing price data for {ticker} on {current_date} or {lookback_date}.")
    if metric_type == 'simple':
        metric = (current_price / lookback_price) - 1
    elif metric_type == 'sma':
        window = prices[ticker].loc[lookback_date:current_date]
        if window.empty:
            raise ValueError(f"No price data for {ticker} between {lookback_date} and {current_date}.")
        sma = window.mean()
        metric = (current_price - sma) / sma
    else:
        raise ValueError("Invalid metric type. Choose 'simple' or 'sma'.")
    return metric

###############################################
# 5. Ranking Assets by Momentum
###############################################

def rank_assets(prices, current_date, tickers, lookback_period, metric_type):
    """
    Computes the lookback metric for each asset and returns sorted tickers, their ranks, and metrics.
    """
    metrics = {}
    for ticker in tickers:
        metric = compute_lookback_metric(prices, current_date, ticker, lookback_period, metric_type)
        metrics[ticker] = metric
    sorted_tickers = sorted(metrics, key=metrics.get, reverse=True)
    ranks = {ticker: rank+1 for rank, ticker in enumerate(sorted_tickers)}
    return sorted_tickers, ranks, metrics

###############################################
# 6. Compute Current Portfolio Value (including Cash)
###############################################

def compute_portfolio_value(portfolio, prices, current_date):
    """
    Returns the portfolio AUM (assets + cash) as of current_date.
    """
    asset_value = sum(quantity * prices.loc[current_date, ticker] 
                      for ticker, quantity in portfolio.items() if ticker != "cash")
    cash = portfolio.get("cash", 0)
    return asset_value + cash

###############################################
# 7. Volatility Scaling: Composite Risk Signal
###############################################

def get_allocation_multiplier(macro_indics, current_date, w_vix, w_credit, w_cons, k, threshold):
    """
    Compute the allocation multiplier based on macro indicators.
    """
    current_date = pd.to_datetime(current_date)
    if not isinstance(macro_indics.index, pd.DatetimeIndex):
        raise ValueError("macro_indics index is not a DatetimeIndex")
    available_dates = macro_indics.index[macro_indics.index <= current_date]
    if len(available_dates) == 0:
        print(f"No macro indicator data available on or before {current_date}")
        return 1.0
    most_recent_date = available_dates[-1]
    current_data = macro_indics.loc[most_recent_date]
    historical_data = macro_indics.loc[:most_recent_date]
    vix_mean, vix_std = historical_data['VIX'].mean(), historical_data['VIX'].std()
    credit_mean, credit_std = historical_data['CreditSpread'].mean(), historical_data['CreditSpread'].std()
    cons_mean, cons_std = historical_data['ConsumerConfidence'].mean(), historical_data['ConsumerConfidence'].std()
    vix_z = (current_data['VIX'] - vix_mean) / vix_std
    credit_z = (current_data['CreditSpread'] - credit_mean) / credit_std
    cons_z = (current_data['ConsumerConfidence'] - cons_mean) / cons_std
    composite_score = w_vix * vix_z + w_credit * credit_z - w_cons * cons_z
    multiplier = 1 / (1 + np.exp(-k * (composite_score - threshold)))
    return multiplier

###############################################
# 8. Rebalance the Portfolio with Cash Allocation
###############################################

def rebalance_portfolio_with_cash(portfolio, prices, current_date, tickers, internal_weights, allocation_multiplier):
    """
    At rebalancing, target:
       - Invest a fraction = allocation_multiplier of the portfolio in assets.
       - Hold the rest as cash.
    
    internal_weights: list of target weights for each asset (should sum to 1).
    """
    # Compute total portfolio value (invested assets + cash)
    total_value = compute_portfolio_value(portfolio, prices, current_date)
    
    # Compute target invested amount and target cash amount.
    target_invested_value = allocation_multiplier * total_value
    target_cash_value = total_value - target_invested_value
    
    new_portfolio = {}
    trades = {}
    
    # For each asset, determine target notional and compute trade notional.
    for i, ticker in enumerate(tickers):
        price = prices.loc[current_date, ticker]
        target_notional = internal_weights[i] * target_invested_value
        current_notional = portfolio.get(ticker, 0) * price
        trade_notional = target_notional - current_notional
        trades[ticker] = trade_notional
        new_portfolio[ticker] = (current_notional + trade_notional) / price
    
    # Set cash position to the target cash.
    new_portfolio["cash"] = target_cash_value
    return new_portfolio, trades, total_value

###############################################
# 9. Simulate the Strategy with Cash Holding
###############################################

def simulate_strategy(prices, eq_tickers, fi_tickers, alts_tickers,
                      initial_aum, start_date, end_date,
                      rebalance_period,
                      lookback_period, metric_type,
                      internal_weights,  # list of weights for assets that sum to 1
                      macro_indics,
                      w_vix=1.0, w_credit=1.0, w_cons=1.0, k=1.0, threshold=0):
    """
    Runs the simulation from start_date to end_date.
    At each rebalancing date:
      - Compute the allocation multiplier from macro indicators.
      - Determine the target invested portion (and thus cash).
      - Rebalance assets to match internal_weights over the target invested amount.
    """
    # Prepare macro_indics DataFrame
    macro_indics.set_index('Date', inplace=True)
    macro_indics.sort_index(inplace=True)

    tickers = eq_tickers + fi_tickers + alts_tickers
    obs_dates = get_observation_dates(start_date, end_date, rebalance_period)
    results = []

    # 9a. Initial portfolio (fully invested; cash = 0)
    portfolio = initialize_portfolio(prices, start_date, tickers, initial_aum)
    portfolio_value = compute_portfolio_value(portfolio, prices, start_date)
    results.append({
        'Date': start_date,
        'Portfolio Value': portfolio_value,
        **{f'qty_{ticker}': portfolio[ticker] for ticker in tickers},
        'cash': portfolio.get("cash", 0),
        'Return': 0,
        'Multiplier': 1.0  # Fully invested initially
    })

    prev_value = portfolio_value

    # 9b. Loop over each observation date after the start date.
    for current_date in obs_dates[1:]:
        multiplier = get_allocation_multiplier(macro_indics, current_date, w_vix, w_credit, w_cons, k, threshold)
        sorted_tickers, ranks, metrics = rank_assets(prices, current_date, tickers, lookback_period, metric_type)
        # Here we ignore ranking for allocation; we simply use the provided internal_weights.
        portfolio, trades, pre_rebalance_value = rebalance_portfolio_with_cash(
            portfolio, prices, current_date, tickers, internal_weights, allocation_multiplier=multiplier)
        portfolio_value = compute_portfolio_value(portfolio, prices, current_date)
        ret = (portfolio_value - prev_value) / prev_value
        prev_value = portfolio_value

        row = {
            'Date': current_date,
            'Portfolio Value': portfolio_value,
            'Return': ret,
            'Pre-Rebalance Value': pre_rebalance_value,
            'Multiplier': multiplier,
            'cash': portfolio.get("cash", 0)
        }
        for ticker in tickers:
            row[f'qty_{ticker}'] = portfolio[ticker]
            row[f'notional_{ticker}'] = portfolio[ticker] * prices.loc[current_date, ticker]
            row[f'weight_{ticker}'] = row[f'notional_{ticker}'] / portfolio_value

            # We keep the ranking and metric info from the momentum calculation if needed.
            row[f'rank_{ticker}'] = ranks.get(ticker, np.nan)
            row[f'metric_{ticker}'] = metrics.get(ticker, np.nan)
            row[f'trade_{ticker}'] = trades.get(ticker, 0)

        results.append(row)

    result_df = pd.DataFrame(results)
    result_df.set_index('Date', inplace=True)
    return result_df

###############################################
# 10. Main – Example Usage
###############################################

if __name__ == '__main__':
    # Define the asset tickers.
    eq_tickers    = ['SPY US Equity']
    fi_tickers    = ['TLT US Equity', 'HYG US Equity']
    alts_tickers  = ['GLD US Equity', 'SHV US Equity', 'VNQ US Equity']
    
    initial_aum = 100e6              # e.g., 100 million
    start_date  = pd.to_datetime('2008-01-01')
    end_date    = pd.to_datetime('2025-02-01')
    rebalance_period = 2             # Rebalance every 2 months
    lookback_period  = 3             # 3-month lookback
    metric_type      = 'simple'      # Use simple return metric
    
    # Define internal weights for assets (must sum to 1).
    # For instance, if you want 70% in the best asset, 30% in the second, etc.
    # Ensure the length matches total number of assets (eq + fi + alts).
    internal_weights = [0.3, 0.3, 0.1, 0.15, 0.15]  # Example weights for 5 assets
    
    # Specify the filepaths for prices and macro indicators.
    price_filepath = r"\\asiapac.nom\data\MUM\IWM\India_IWM_IPAS\Reet\Momentum Strategy\Codes\Historic Prices.xlsx"
    prices = load_price_data(price_filepath)
    
    macro_filepath = r"\\asiapac.nom\data\MUM\IWM\India_IWM_IPAS\Reet\Momentum Strategy\macro_indic.xlsx"
    macro_indics = load_macro_indics(macro_filepath)
    macro_indics = macro_indics.reset_index()
    
    # Run the simulation.
    # Here, if the multiplier is low (e.g., 0.5) then only 50% of the portfolio is invested,
    # and the remaining 50% is held in cash until the next rebalance.
    result_df = simulate_strategy(prices, eq_tickers, fi_tickers, alts_tickers,
                                  initial_aum, start_date, end_date,
                                  rebalance_period, lookback_period, metric_type,
                                  internal_weights,
                                  macro_indics,
                                  w_vix=1.3, w_credit=0.9, w_cons=1.1, k=0.1, threshold=-0.3)
    
    # Display the final results.
    pd.set_option('display.float_format', lambda x: f'{x:,.2f}')
    print(result_df)
Editor is loading...
Leave a Comment