Untitled

 avatar
unknown
plain_text
a month ago
30 kB
2
Indexable
import pandas as pd
import numpy as np
from datetime import datetime
from dateutil.relativedelta import relativedelta
from scipy.stats import zscore

###############################################
# 1. Data Loading (Filtering Out Weekends)
###############################################

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.
    Removes weekend data.
    """
    df = pd.read_excel(filepath, index_col=0)
    df.index = pd.to_datetime(df.index)
    df = df.sort_index()
    # Filter out weekends (Saturday=5, Sunday=6)
    df = df[df.index.dayofweek < 5]
    return df

def load_macro_data(filepath):
    """
    Load macro indicators from an Excel file.
    Loads VIX and VIX3M from 'Eq' sheet, LF98TRUU Index from 'FI' sheet, 
    and other macro indicators from 'Macro' sheet.
    Removes weekend data.
    Also computes slopes for selected macro indicators.
    """
    # VIX data
    vix_data = pd.read_excel(filepath, sheet_name='Eq', index_col=0, parse_dates=True, usecols=[0, 3, 4, 5])
    vix_data.columns = ['VIX9D', 'VIX', 'VIX3M']
    vix_data = vix_data[vix_data.index.dayofweek < 5]
    
    # FI data
    cdx_data = pd.read_excel(filepath, sheet_name='FI', index_col=0, parse_dates=True, usecols=[0, 2], skiprows=1)
    cdx_data.columns = ['LF98TRUU']
    cdx_data = cdx_data[cdx_data.index.dayofweek < 5]
    
    # Macro data (assumed to include columns "CESIUSD Index", "INJCJC Index", ".HG/GC G Index", and "Consumer Confidence")
    macro_data = pd.read_excel(filepath, sheet_name='Macro', index_col=0, parse_dates=True, usecols=range(8), skiprows=1)
    macro_data = macro_data[macro_data.index.dayofweek < 5]
    
    # Compute slopes for selected macro indicators.
    macro_data["Surprise Index Slope"] = macro_data["CESIUSD Index"].diff()
    macro_data["Jobless Claims Slope"] = macro_data["INJCJC Index"].diff()
    macro_data["Copper Gold Slope"] = macro_data['.HG/GC G Index'].diff()
    # Assume "Consumer Confidence" column already exists.
    
    combined_data = pd.concat([vix_data, cdx_data, macro_data], axis=1)
    combined_data = combined_data.fillna(method='ffill').fillna(method='bfill')
    combined_data = combined_data.sort_index()  # ensure sorted index
    return combined_data

###############################################
# 2. Helper: Observation Dates (Monthly)
###############################################

def get_observation_dates(prices, start_date, end_date, rebalance_period):
    """
    Returns observation dates using the trading calendar.
    For a monthly rebalancing (rebalance_period=1), for each target month, 
    it starts at the first calendar day and checks sequentially until it finds a trading day.
    
    Parameters:
      prices: DataFrame with a DatetimeIndex containing trading days.
      start_date: Starting date (should be a valid trading day).
      end_date: End date for observations.
      rebalance_period: Number of months between rebalances.
      
    Returns:
      List of observation dates.
    """
    dates = []
    current_date = start_date

    while current_date < end_date:
        # Move forward by the rebalance period (in months) and set candidate to the first day of that month
        candidate_date = (current_date + relativedelta(months=rebalance_period)).replace(day=1)
        
        # Check sequentially until a trading day is found in prices index
        while candidate_date not in prices.index:
            candidate_date += pd.Timedelta(days=1)
            # Safety: if candidate_date moves into the next month, break out (unlikely if market is active)
            if candidate_date.month != (current_date + relativedelta(months=rebalance_period)).month:
                candidate_date = None
                break
        
        if candidate_date is None or candidate_date > end_date:
            break
        
        dates.append(candidate_date)
        current_date = candidate_date

    return dates

###############################################
# 3. Portfolio Initialization
###############################################

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.
    """
    portfolio = {}
    mask = prices.index < date
    prev_date = prices.index[mask][-1]
    allocation = initial_aum / len(tickers)
    for ticker in tickers:
        price = prices.loc[prev_date, ticker]
        portfolio[ticker] = allocation / price
    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 using previous day's data.
    """
    prices = prices.sort_index()
    mask = prices.index < current_date
    prev_date = prices.index[mask][-1]

    lookback_date = prev_date - relativedelta(months=lookback_period)
    
    current_price = prices[ticker].asof(prev_date)  # Use previous day's price
    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 {prev_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):
    """
    For a given observation date, compute the chosen lookback metric for each asset,
    then sort (in descending order) so that the highest momentum gets rank 1.
    
    Returns:
      sorted_tickers: list of tickers in sorted order (best first)
      ranks: dictionary mapping ticker -> rank (1 is best)
      metrics: dictionary mapping ticker -> computed metric value
    """
    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
###############################################

def compute_portfolio_value(portfolio, prices, current_date):
    """
    Returns the portfolio AUM as of current_date.
    """
    value = 0
    for ticker, quantity in portfolio.items():
        price = prices.loc[current_date, ticker]
        value += quantity * price
    return value

###############################################
# 7. Rebalance the Momentum Portfolio
###############################################

def rebalance_portfolio(portfolio, prices, current_date, tickers, sorted_tickers,
                        internal_rebalance_ratios, rebalance_ratio):
    prev_date = prices.index[prices.index < current_date][-1]
    prev_prices = prices.loc[prev_date]
    curr_prices = prices.loc[current_date]

    portfolio_value = sum(portfolio[ticker] * prev_prices[ticker] for ticker in tickers)
    rebalance_amount = portfolio_value * rebalance_ratio

    target_trades = {ticker: rebalance_amount * internal_rebalance_ratios[i]
                     for i, ticker in enumerate(sorted_tickers)}

    total_sold = 0
    actual_trades = {}

    for ticker, target_trade in target_trades.items():
        execution_price = curr_prices[ticker]
        if target_trade < 0:
            available_notional = portfolio[ticker] * curr_prices[ticker]
            sell_target = abs(target_trade)
            actual_sell = min(available_notional, sell_target)
            actual_trades[ticker] = -actual_sell
            total_sold += actual_sell
        else:
            actual_trades[ticker] = 0

    total_buy_target = sum(t for t in target_trades.values() if t > 0)
    if total_buy_target > 0:
        for ticker, target_trade in target_trades.items():
            if target_trade > 0:
                proportion = target_trade / total_buy_target
                buy_amount = total_sold * proportion
                actual_trades[ticker] = buy_amount

    new_portfolio = portfolio.copy()
    for ticker, trade_notional in actual_trades.items():
        execution_price = curr_prices[ticker]
        qty_change = trade_notional / execution_price
        new_portfolio[ticker] += qty_change

    return new_portfolio, actual_trades, portfolio_value

def adjust_overweight(portfolio, prices, current_date, sorted_tickers, threshold=0.70):
    
    prev_date = prices.index[prices.index < current_date][-1]
    prev_prices = prices.loc[prev_date]
    curr_prices = prices.loc[current_date]

    portfolio_value = sum(portfolio[ticker] * prev_prices[ticker] for ticker in portfolio)
    weights = {ticker: (portfolio[ticker] * prev_prices[ticker]) / portfolio_value
               for ticker in portfolio}

    new_portfolio = portfolio.copy()
    adjustments = {}

    for overweight in portfolio:
        if weights[overweight] > threshold:
            extra_weight = weights[overweight] - threshold
            extra_value = extra_weight * portfolio_value
            execution_price_over = curr_prices[overweight]
            qty_reduce = extra_value / execution_price_over
            new_portfolio[overweight] -= qty_reduce

            adjustments[overweight] = {'removed_qty': qty_reduce, 'reallocated': {}}
            remaining_value = extra_value

            for candidate in sorted_tickers:
                if candidate == overweight:
                    continue
                candidate_value = new_portfolio[candidate] * curr_prices[candidate]
                candidate_weight = candidate_value / portfolio_value

                if candidate_weight < threshold:
                    capacity = (threshold - candidate_weight) * portfolio_value
                    allocation = min(remaining_value, capacity)
                    qty_add = allocation / curr_prices[candidate]
                    new_portfolio[candidate] += qty_add

                    adjustments[overweight]['reallocated'][candidate] = qty_add
                    remaining_value -= allocation

                    if remaining_value <= 0:
                        break

            if remaining_value > 0:
                adjustments[overweight]['leftover_value'] = remaining_value

    return new_portfolio

###############################################
# 8. VIX-based Allocation Function
###############################################

def momentum_allocation(vix_9d, vix_mean, vix_std, max_alloc=1.0, min_alloc=0.6):
    """
    Maps composite score to a target momentum allocation fraction using a piecewise linear approach.
    
    - If vix_9d < vix_mean + vix_std, return max_alloc (fully risk on).
    - If vix_9d >= vix_mean + 2*vix_std, return min_alloc (fully risk off).
    - Otherwise, return 0.8 as an intermediate allocation.
    """
    if vix_9d < vix_mean + vix_std:
        return max_alloc
    elif (vix_9d >= vix_mean + vix_std) and (vix_9d < vix_mean + 2*vix_std):
        return 0.8
    else:
        return min_alloc
    
###############################################
# 9. Compute FI Signal Functions
###############################################

def compute_fi_target_allocation(macro_data, current_date, fi_max_alloc, fi_min_alloc, slope_threshold=0.01):
    """
    Computes the FI target allocation based on a refined logic using only the 8-day and 13-day EMAs.
    
    The computation uses data only up to the previous trading day.
    
    Returns:
      target_alloc: the target allocation.
      signal_label: a string label ("risk-on", "neutral", or "risk-off").
    """
    # Determine the reference date as the previous trading day.
    available_dates = macro_data.index[macro_data.index < current_date]
    if len(available_dates) == 0:
        return fi_max_alloc, "risk-on"
    ref_date = available_dates[-1]
    fi_8 = macro_data["FI_EMA_8"].asof(ref_date)
    fi_13 = macro_data["FI_EMA_13"].asof(ref_date)
    
    if pd.isna(fi_8) or pd.isna(fi_13):
        return fi_max_alloc, "risk-on"
    
    # Compute slope of FI_EMA_8 using ref_date.
    available_ref_dates = macro_data.loc[:ref_date].index
    if len(available_ref_dates) < 2:
        slope = 0
    else:
        prev_ref_date = available_ref_dates[-2]
        fi_8_prev = macro_data["FI_EMA_8"].asof(prev_ref_date)
        slope = (fi_8/fi_8_prev) - 1
    
    if fi_8 < fi_13:
        return fi_max_alloc, "risk-on"
    else:  # fi_8 > fi_13
        if slope > slope_threshold:
            return fi_min_alloc, "risk-off"
        else:
            return fi_max_alloc, "no signal"

###############################################
# 10. Simulation: VIX-based (current) Strategy with Signal Logging
###############################################

def simulate_strategy(prices, macro_data, eq_tickers, fi_tickers, alts_tickers,
                      initial_aum, start_date, end_date,
                      rebalance_period, rebalance_ratio,
                      lookback_period, metric_type,
                      internal_rebalance_ratios,
                      macro_max_alloc=1.0, macro_min_alloc=0.6):
    """
    Runs the simulation with:
      - Daily macro overlay adjustments
      - Monthly momentum rebalancing based on total returns.
    
    This version computes and logs the following signals daily:
      • VIX signal (and corresponding target allocation)
      • Composite macro signal (and target allocation)
      • FI signal (and target allocation)
    """
    
    tickers = eq_tickers + fi_tickers + alts_tickers
    monthly_dates = get_observation_dates(prices, start_date, end_date, rebalance_period)
    daily_dates = prices.index.sort_values()
    
    
    # initiate vars
    vix_target_alloc = macro_max_alloc  # Default to max allocation
    vix_signal = "risk-on"              # Default to risk-on
    macro_target_alloc = macro_max_alloc
    macro_signal = "risk-on"
    fi_target_alloc = macro_max_alloc
    fi_signal = "no signal"
    
    # Prepare macro data: compute VIX EMA fields
    macro_data = macro_data.copy()
    macro_data['VIX 1M 3M_Spread'] = macro_data['VIX'] - macro_data['VIX3M']
    macro_data['VIX1M_EMA'] = macro_data["VIX 1M 3M_Spread"].ewm(span=5, adjust=False).mean()  
    macro_data["Mean"] = macro_data['VIX1M_EMA'].rolling(window=504).mean()
    macro_data["Std"] = macro_data['VIX1M_EMA'].rolling(window=504).std() 
    
    # Precompute FI EMAs
    macro_data["FI_EMA_8"] = macro_data["LF98TRUU"].ewm(span=8, adjust=False).mean()
    macro_data["FI_EMA_13"] = macro_data["LF98TRUU"].ewm(span=13, adjust=False).mean()
    macro_data["FI_EMA_21"] = macro_data["LF98TRUU"].ewm(span=21, adjust=False).mean()
    
    portfolio = initialize_portfolio(prices, start_date, tickers, initial_aum)
    CASH = 0.0
    current_regime = 'risk-on'
    target_alloc = macro_max_alloc
    previous_regime = current_regime
    prev_total_aum = initial_aum
    ret = 0.0
    
    previous_regime = current_regime
    
    results = []
    prev_total_aum = initial_aum
    
    # Filter daily_dates to ensure they're within range
    daily_dates = daily_dates[(daily_dates >= start_date) & (daily_dates <= end_date)]
    
    for current_date in daily_dates:
        daily_adjustment_note = "No adjustment"
        cash_adjustment = 0
        
        # Determine reference date (previous trading day)
        available_macro_dates = macro_data.index[macro_data.index < current_date]
        print
        if len(available_macro_dates) == 0:
            ref_date = current_date
        else:
            ref_date = available_macro_dates[-1]
        
        # Calculate FI metrics
        fi_8 = macro_data["FI_EMA_8"].asof(ref_date)
        fi_13 = macro_data["FI_EMA_13"].asof(ref_date)

        # Calculate FI slope
        available_ref_dates = macro_data.loc[:ref_date].index
        if len(available_ref_dates) < 2:
            fi_slope = 0
        else:
            prev_ref_date = available_ref_dates[-2]
            fi_8_prev = macro_data["FI_EMA_8"].asof(prev_ref_date)
            fi_slope = (fi_8/fi_8_prev) - 1

        # Get VIX metrics
        vix_1m = macro_data['VIX'].asof(ref_date)
        vix_3m = macro_data['VIX3M'].asof(ref_date)

        # Calculate z-scores
        vix_window = macro_data.loc[:ref_date].tail(504)  # Using 2-year window (504 trading days)
        vix_1m_zscore = zscore(vix_window['VIX'])[-1] if len(vix_window) > 0 else np.nan
        vix_3m_zscore = zscore(vix_window['VIX3M'])[-1] if len(vix_window) > 0 else np.nan
        
        # Daily macro signal computation and adjustment using ref_date
        try:
            vix_ema = macro_data['VIX1M_EMA'].asof(ref_date)
            vix_mean = macro_data['Mean'].asof(ref_date)
            vix_std = macro_data['Std'].asof(ref_date)
        except Exception as e:
            raise ValueError(f"Error retrieving VIX data for {current_date}: {e}")
            
        vix_target_alloc = momentum_allocation(vix_ema, vix_mean, vix_std,
                                               max_alloc=macro_max_alloc, 
                                               min_alloc=macro_min_alloc)

        if vix_ema >= (vix_mean + vix_std):
            vix_signal = 'risk-off'
        elif vix_ema <= (vix_mean - vix_std):
            vix_signal = 'risk-on'
        else:
            vix_signal = 'no-signal'

        
        # Compute FI Signal using data up to ref_date
        fi_target_alloc, fi_signal = compute_fi_target_allocation(
            macro_data, current_date, macro_max_alloc, macro_min_alloc, slope_threshold=0.01)
        
        # ---- Daily Check: Force regime if SPY weight is low and current regime is risk-off ----
        mask = prices.index < current_date
        prev_date = prices.index[mask][-1]
        mom_value = compute_portfolio_value(portfolio, prices, prev_date)
        spy_price = prices.loc[prev_date, 'SPY US Equity']
        spy_qty = portfolio.get('SPY US Equity', 0)
        spy_weight = (spy_price * spy_qty) / mom_value if mom_value > 0 else 0
        
        hyg_price = prices.loc[prev_date, 'HYG US Equity']
        hyg_qty = portfolio.get('HYG US Equity', 0)
        hyg_weight = (hyg_price * hyg_qty) / mom_value if mom_value > 0 else 0
        
        if (fi_signal == 'risk-off' or vix_signal == 'risk-off'):
            if ((spy_weight + hyg_weight) < 0.40):
                current_regime = 'risk-on'
                target_alloc = 1.0
                daily_adjustment_note = "Forced regime to risk-on & target alloc to 1 due to HYG + SPY weight < 40%"
            else:
                current_regime = 'risk-off'
                target_alloc = min(fi_target_alloc, vix_target_alloc)
        else:
            current_regime = 'risk-on'
        
        # Use VIX signal for adjustment if regime changed.
        if current_regime != previous_regime:
            mask = prices.index < current_date
            prev_date = prices.index[mask][-1]
            mom_value = compute_portfolio_value(portfolio, prices, prev_date)
            total_investment = mom_value + CASH
            desired_mom_value = target_alloc * total_investment
            
            mom_value = compute_portfolio_value(portfolio, prices, current_date)
            
            if mom_value > desired_mom_value:
                excess = mom_value - desired_mom_value
                cash_adjustment = excess
                for ticker in portfolio:
                    price = prices.loc[current_date, ticker]
                    ticker_value = portfolio[ticker] * price
                    sell_amount = (ticker_value / mom_value) * excess
                    qty_to_sell = sell_amount / price
                    portfolio[ticker] -= qty_to_sell
                CASH += excess
                daily_adjustment_note = f"Adj: Sold excess {excess:.2f} to CASH."
            elif mom_value < desired_mom_value and CASH > 0:
                shortage = desired_mom_value - mom_value
                available = min(shortage, CASH)
                cash_adjustment = -available
                for ticker in portfolio:
                    price = prices.loc[current_date, ticker]
                    ticker_value = portfolio[ticker] * price
                    target_weight = ticker_value / mom_value if mom_value > 0 else 1/len(portfolio)
                    invest_amount = target_weight * available
                    qty_to_buy = invest_amount / price
                    portfolio[ticker] += qty_to_buy
                CASH -= available
                daily_adjustment_note = f"Adj: Bought using {available:.2f} CASH."
        
        # Monthly momentum rebalancing block.
        if current_date in monthly_dates:
            
            sorted_tickers, ranks, metrics = rank_assets(
                prices, current_date, tickers, lookback_period, metric_type)
            portfolio, trades, pre_rebalance_value = rebalance_portfolio(
                portfolio, prices, current_date, tickers, sorted_tickers,
                internal_rebalance_ratios, rebalance_ratio)
            
            mask = prices.index < current_date
            prev_date = prices.index[mask][-1]
            mom_value = compute_portfolio_value(portfolio, prices, prev_date)
            total_aum = mom_value + CASH
            desired_mom_value = target_alloc * total_aum
            mom_value = compute_portfolio_value(portfolio, prices, current_date)
            
            
            if mom_value > desired_mom_value:
                excess = mom_value - desired_mom_value
                cash_adjustment = excess
                for ticker in portfolio:
                    price = prices.loc[current_date, ticker]
                    ticker_value = portfolio[ticker] * price
                    sell_amount = (ticker_value / mom_value) * excess
                    qty_to_sell = sell_amount / price
                    portfolio[ticker] -= qty_to_sell
                CASH += excess
                daily_adjustment_note = f"Monthly: Sold excess {excess:.2f} to CASH."
            elif mom_value < desired_mom_value and CASH > 0:
                shortage = desired_mom_value - mom_value
                available = min(shortage, CASH)
                cash_adjustment = -available
                for ticker in portfolio:
                    price = prices.loc[current_date, ticker]
                    ticker_value = portfolio[ticker] * price
                    target_weight = ticker_value / mom_value if mom_value > 0 else 1/len(portfolio)
                    invest_amount = target_weight * available
                    qty_to_buy = invest_amount / price
                    portfolio[ticker] += qty_to_buy
                CASH -= available
                daily_adjustment_note = f"Monthly: Bought using {available:.2f} CASH."
            else:
                daily_adjustment_note = "Monthly: No cash adjustment needed."
            
            # Overweight Adjustment Process.
            portfolio = adjust_overweight(portfolio, prices, current_date, sorted_tickers, threshold=0.70)
            
            # Recalculate portfolio value after overweight adjustments.
            mom_value = compute_portfolio_value(portfolio, prices, current_date)
            total_aum = mom_value + CASH
            ret = (total_aum - prev_total_aum) / prev_total_aum
            prev_total_aum = total_aum
            
            # Log monthly rebalancing event with additional metrics
            row = {
                'Date': current_date,
                'Momentum AUM': mom_value,
                'CASH': CASH,
                'Total AUM': total_aum,
                'Current Regime (VIX)': current_regime,
                'VIX Target': vix_target_alloc,
                'VIX Signal': vix_signal,
                'FI Target': fi_target_alloc,
                'FI Signal': fi_signal,
                'Target Momentum Alloc': target_alloc,
                'Adjustment Note': daily_adjustment_note,
                'Cash Adjustment': cash_adjustment,
                'Return': ret,
                'Event': 'Monthly Rebalance',
                'FI_EMA_8': fi_8,
                'FI_EMA_13': fi_13,
                'FI_Slope': fi_slope,
                'VIX_1M': vix_1m,
                'VIX_3M': vix_3m,
                'VIX_1M_ZScore': vix_1m_zscore,
                'VIX_3M_ZScore': vix_3m_zscore
            }
            
            for ticker in tickers:
                price = prices.loc[current_date, ticker]
                qty = portfolio[ticker]
                notional = qty * price
                row[f'qty_{ticker}'] = qty
                row[f'notional_{ticker}'] = notional
                row[f'weight_{ticker}'] = (notional / mom_value) if mom_value > 0 else np.nan
                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)
        else:
            # Log daily signal checks with additional metrics
            current_mom_value = compute_portfolio_value(portfolio, prices, current_date)
            total_aum = current_mom_value + CASH
            ret = (total_aum - prev_total_aum) / prev_total_aum if prev_total_aum > 0 else 0
            prev_total_aum = total_aum
            row = {
                'Date': current_date,
                'Momentum AUM': current_mom_value,
                'CASH': CASH,
                'Total AUM': total_aum,
                'Current Regime (VIX)': current_regime,
                'VIX Target': vix_target_alloc,
                'VIX Signal': vix_signal,
                'FI Target': fi_target_alloc,
                'FI Signal': fi_signal,
                'Target Momentum Alloc': target_alloc,
                'Adjustment Note': daily_adjustment_note,
                'Cash Adjustment': cash_adjustment,
                'Return': ret,
                'Event': 'Daily Check',
                'FI_EMA_8': fi_8,
                'FI_EMA_13': fi_13,
                'FI_Slope': fi_slope,
                'VIX_1M': vix_1m,
                'VIX_3M': vix_3m,
                'VIX_1M_ZScore': vix_1m_zscore,
                'VIX_3M_ZScore': vix_3m_zscore
            }
            
            for ticker in tickers:
                price = prices.loc[current_date, ticker]
                qty = portfolio[ticker]
                notional = qty * price
                row[f'qty_{ticker}'] = qty
                row[f'notional_{ticker}'] = notional
                row[f'weight_{ticker}'] = (notional / current_mom_value) if current_mom_value > 0 else np.nan
                row[f'rank_{ticker}'] = np.nan
                row[f'metric_{ticker}'] = np.nan
                row[f'trade_{ticker}'] = 0
            results.append(row)
    
    result_df = pd.DataFrame(results)
    result_df.set_index('Date', inplace=True)
    return result_df

###############################################
# 11. Main – Example Usage
###############################################

if __name__ == '__main__':
    # Define asset tickers.
    eq_tickers   = ['SPY US Equity']
    fi_tickers   = ['TLT US Equity', 'HYG US Equity']
    alts_tickers = ['GLD US Equity', 'SHV US Equity']
   
    initial_aum = 100e6   # 100 million
    start_date  = pd.to_datetime('2008-01-01')
    end_date    = pd.to_datetime('2025-02-01')
    rebalance_period = 1      # monthly (or adjust as desired)
    rebalance_ratio  = 0.1  # 20% of current momentum AUM rebalanced each period
    lookback_period  = 6      
    metric_type      = 'simple'
    
    internal_rebalance_ratios = [0.8, 0.2, 0, -0.2, -0.8]
    
    # File paths (adjust these to your environment).
    price_filepath = r"\\asiapac.nom\data\MUM\IWM\India_IWM_IPAS\Reet\Momentum Strategy\Codes\Historic Prices.xlsx"
    macro_filepath = r"\\asiapac.nom\data\MUM\IWM\India_IWM_IPAS\Reet\Momentum Strategy\Momentum Strategy Overlay Data.xlsx"
    
    prices = load_price_data(price_filepath)
    macro_data = load_macro_data(macro_filepath)
    
    # Run simulation.
    result_df = simulate_strategy(prices, macro_data,
                                  eq_tickers, fi_tickers, alts_tickers,
                                  initial_aum, start_date, end_date,
                                  rebalance_period, rebalance_ratio,
                                  lookback_period, metric_type,
                                  internal_rebalance_ratios,
                                  macro_max_alloc=1.0, macro_min_alloc=0.6)
    
    pd.set_option('display.float_format', lambda x: f'{x:,.2f}')
    print(result_df)
Editor is loading...
Leave a Comment