Quant's VLRT under the hood

I never liked Quant flex cap ever since I first heard of it.

To me the reason to buy an active fund is philosophy - ie Value, growth, quality, momentum etc.

Without this philosophy, an active fund is simply a trader in my view - and on the surface level, since Quant showcased itself as a magical crystal ball that can predict the future - my red flags were immediately waving.

But I ignored it - why look at a fund manager who you disagree with?

However, the fund kept coming back to me with people on reddit asking opinions, to it being listed as the best performer etc - and I just commented back it is not something I personally like without giving a reason.

But then Value Research hosted Sandeep Tandon - and that was the last straw that broke the camel's back.

I decided to look under the hood:

1. High Portfolio Turnover Ratio

2. Aggressive Sector Rotation based on high-beta stocks

3. Liquidity and Risk Appetite (as described by quant of VLRT model)

These characteristics essentially mimics what a momentum play does.

And so with a hypothesis that Quant's flexi cap is basically a momentum play, I benchmarked it against the Momentum Funds.

Below is the Trailing returns between Quant's flexi cap and Nifty 200 momentum 30 index, as per value research :

Period Quant Flexi Cap Fund UTI Nifty 200 Momentum 30 Index fund Motilal Oswal Nifty 500 Momentum 50 index fund
1 Year 19% 5.54% 5.78%
3 Years 17.57% 13.49% 12.74%
5 Years 16.39% 11.24% 11.88%

But even a 5 year  was not coming close, and So I had to use AI's help.

Upon discussing, It was able to map Quant Flexi cap to Nifty 500 momentum 50 TRI.

According to it, the Values would go similar to this :

Period Quant Flexi Cap Fund Nifty 500 momentum 50 Tri
1 Year 18.85%9.33%
3 Years 18.12% 13.85%
5 Years 16.19% 15.02%

Finally, we are getting somewhere, in a 5 year something closer to what Quant achieved.

But as you can see we have slight discrepancies in what the Value Research reported and what the AI was reporting even for the Quant -  not much but slight variance.

And so I decided to perform a rolling return analysis using a Python script, by pulling the NAV from MFAPI - keep in mind that I am still unable to benchmark it against the TRI, but  I will be able to benchmark it against the index funds to analyze how it performs.

Below are the results : 

1 Year rolling return comparison between Quant, Nifty 200 momentum 30 and Nifty 500 momentum 50


Since the Nifty 500 momentum 50 is fairly new,  it doesn't provide us with a long enough time frame  to perform a detailed analysis - but looking at the chart, it does appear to be matching the momentum's 1 year rolling returns while providing a some downside protection.

But I am skeptical about the predictive capabilities, since as you can see, it show weakness when the momentum factor shows weakness.

But it reacts to the weakness and attempts  to not let it slide to the negative territory, while the index  simply doesn't have any such downside protection mechanisms.

But to get a better vantage, I removed the Nifty 500 momentum 50, so that I have a better time frame to analyze.

So comparing the 1Y rolling return  between Quant and Nifty 200 momentum 30 starts to show the real picture.

Looking at it, it is clear that that the fund is performing a downside protection, and due to the hedging, it fails to capture the peaks of the momentum either.

And watching this tells us exactly why they faced the rescent -16% crash and the 32% growth - it has little to do with the custom algorithm, but because the underlying momentum did that.

The same can be said about the 3Y rolling return


Again we can see that the Quant is following the same trajectory as the Momentum fund. 
But here, we can see the advantage of the downside protection showing up, it starts to mosstly beat the market over longer periods.

I also ran a 5 year rolling return as well : 


Now here, you can see that Quant  is able to beat the Nifty 200 momentum  nicely, but keep in mind that this is only 3 months of data -  it is not enough to convincingly say that is consistently generating the alpha.

And so my conclusion to the analysis is that Quant is an Active Momentum Fund, the VLRT might be a framework, but it is not operating on predictive model, it is a reactive model - its returns are purely based on the momentum play and when it detects a downfall, it hedges to stop the downward movement.

I would classify it as an Active Momentum Fund, which provides a smooth ride compared to the momentum fund.

While this analysis, did give a better impression of the fund to me, the fact that the fund manager claims predictive capabilities remains concerning. 

Additionally, the exact algorithm they use is not an open-source globally back tested algorithm - but a proprietary one - and so for me, it is still too new for me to blindly trust -  we don't have enough data to convincingly state the fund can outperform the index consistently.

But yes, over a 1year of a 3 year period,  it appears to be holding well.

But the concerning part remains the proprietary algorithm, How exactly are they providing the downside protection remains the unanswered question.

Are they taking risky calls during the downturn to put an emergency stop to it? 

While every AMCs use their proprietary algorithms, the underlying structure is clear -  we are investing in the Fund manager's conviction and philosophy and not the algorithms.

But when it comes to a momentum strategy, unfortunately, it cannot claim any such human conviction - as it is mostly dictated mathematically and the philosophy is buy high and sell higher, which is a very dangerous philosophy to have.

Therefore, without the underlying algorithm, it is difficult to guarantee whether they are adopting riskier practices to stop the downfalls and is different than other active funds.

And so while the Fund itself is delivering impressive strategy, that I could potentially back -  the lack of transparency of the fund manager, to not call it a momentum strategy, but a predictive strategy becomes the deciding factor for me.

It would have been a perfect fund, if the manager had claimed an active momentum strategy than a crystal ball.

I would  have expected them to benchmark it appropriately and provide general hints on how he is providing the downside protection - like whether he is moving to value or using Future  and option.

To me that remains a black box and don't know whether they are taking additional risks to protect the downfall -  and so I must remain not invested in them.




1. Below is the code used to generate the rollng returns, feel free to check it : 

import datetime
import numpy as np
import pandas as pd
import requests
import matplotlib.pyplot as plt

def fetch_mf_nav(scheme_code, name):
    """Fetches historical NAV data for an Indian Mutual Fund from mfapi.in."""
    url = f'https://api.mfapi.in/mf/{scheme_code}'
    response = requests.get(url)
    if response.status_code != 200:
        raise ValueError(f'Failed to fetch data for {name} (code: {scheme_code})')

    data = response.json()
    if 'data' not in data or not data['data']:
        raise ValueError(f'No data returned for {name} (code: {scheme_code})')

    df = pd.DataFrame(data['data'])
    df['date'] = pd.to_datetime(df['date'], format='%d-%m-%Y')
    df['nav'] = pd.to_numeric(df['nav'])

    df = df.sort_values('date').reset_index(drop=True)
    df.set_index('date', inplace=True)
    return df[['nav']].rename(columns={'nav': name})

def calculate_rolling_cagr(series, years):
    """Calculates annualized rolling CAGR for a specific year window."""
    window_days = int(years * 252) # Approximate trading days
    return (series / series.shift(window_days)) ** (1 / years) - 1

def main():
    # Setup AMFI Codes (Direct Growth variants)
    FUNDS = {
        'Quant Flexi Cap': '120843',
        'Nifty 500 Momentum 50 (Motilal)': '152875', 
        'Nifty 200 Momentum 30 (UTI)': '148703'
    }

    # 1. Fetch all data
    raw_data = {}
    print("Fetching data from mfapi.in...")
    for name, code in FUNDS.items():
        try:
            print(f" -> Fetching {name}...")
            raw_data[name] = fetch_mf_nav(code, name)
        except Exception as e:
            print(f"Error fetching {name}: {e}")
            return

    windows = [1, 3, 5, 10]
    
    # Define distinct colors for the lines
    colors = {
        'Quant Flexi Cap': '#8A2BE2',                  # Purple
        'Nifty 500 Momentum 50 (Motilal)': '#FF8C00',  # Orange
        'Nifty 200 Momentum 30 (UTI)': '#008B8B'       # Dark Cyan
    }

    # 2. Process each rolling window independently
    for y in windows:
        print(f"\n{'-'*50}\nProcessing {y}-Year Rolling Return...")
        window_days = int(y * 252)
        
        # Determine which funds actually have enough historical data for this window
        qualifying_funds = {}
        for name, df in raw_data.items():
            if len(df) > window_days:
                qualifying_funds[name] = df
            else:
                print(f"  [Dropped] '{name}' does not have {y} years of history.")
                
        # We need at least 2 funds to make a comparison chart
        if len(qualifying_funds) < 2:
            print(f"  [Skipped] Not enough qualifying funds to compare for {y}-year window.")
            continue

        # Setup combinations to run. Default is all qualifying funds.
        combinations_to_run = [("Standard", qualifying_funds)]
        
        # ADDITION: For 1-Year, add a second run that specifically excludes Motilal for deeper history.
        if y == 1 and 'Nifty 500 Momentum 50 (Motilal)' in qualifying_funds:
            qualifying_without_motilal = {k: v for k, v in qualifying_funds.items() if k != 'Nifty 500 Momentum 50 (Motilal)'}
            if len(qualifying_without_motilal) >= 2:
                combinations_to_run.append(("Extended History", qualifying_without_motilal))

        for run_type, funds_subset in combinations_to_run:
            if run_type == "Extended History":
                print(f"\n  -> Running additional {y}-Year comparison (Excluding Nifty 500 Momentum 50 for longer history)...")
            
            # 3. Find the youngest start date among the *subset* funds
            start_dates = [df.index.min() for df in funds_subset.values()]
            youngest_start = max(start_dates)
            
            if run_type == "Standard":
                print(f"  -> Qualifying funds aligned to youngest start date: {youngest_start.date()}")
            else:
                print(f"  -> Subset aligned to youngest start date: {youngest_start.date()}")

            # 4. Filter, Merge, and Calculate
            aligned_dfs = []
            for name, df in funds_subset.items():
                # Slice from the youngest start date forward
                sliced_df = df[df.index >= youngest_start]
                aligned_dfs.append(sliced_df)
                
            # Join them on exact trading dates
            combined_df = aligned_dfs[0]
            for df in aligned_dfs[1:]:
                combined_df = combined_df.join(df, how='inner')
                
            combined_df.dropna(inplace=True)

            if combined_df.empty:
                print("  [Error] No overlapping data found after alignment.")
                continue

            # Calculate the rolling returns (%)
            plot_df = pd.DataFrame(index=combined_df.index)
            for name in funds_subset.keys():
                plot_df[name] = calculate_rolling_cagr(combined_df[name], y) * 100
                
            plot_df.dropna(inplace=True)
            
            if plot_df.empty:
                print(f"  [Skipped] Insufficient overlap to generate {y}-year rolling data.")
                continue

            # 5. Generate the Chart
            plt.figure(figsize=(12, 6))
            
            for column in plot_df.columns:
                plt.plot(plot_df.index, plot_df[column], label=column, color=colors[column], linewidth=1.5)
            
            title_text = f'{y}-Year Rolling Return CAGR (%)\n(Baseline alignment: {youngest_start.date()})'
            if run_type == "Extended History":
                title_text += " - Excl. Nifty 500 Momentum 50"
                
            plt.title(title_text, fontsize=14, pad=15)
            plt.xlabel('Date', fontsize=12)
            plt.ylabel('Rolling CAGR (%)', fontsize=12)
            plt.axhline(0, color='black', linestyle='--', linewidth=0.8, alpha=0.7)
            plt.legend(loc='best', fontsize=11)
            plt.grid(True, alpha=0.3)
            plt.tight_layout()
            
            # Save to PNG
            suffix = "_extended_history" if run_type == "Extended History" else ""
            filename = f"rolling_returns_{y}Y_comparison{suffix}.png"
            plt.savefig(filename, dpi=300)
            plt.close()
            print(f"  [Success] Chart generated: {filename}")

if __name__ == '__main__':
    main()

Comments

Popular posts from this blog

How to start Investing

4% Safe Withdrawal Rate Is Valid For India

I Bought Hundreds Of Funds & It Didn't Diworsify