Skip to content

Latest commit

 

History

History
1231 lines (849 loc) · 43.6 KB

File metadata and controls

1231 lines (849 loc) · 43.6 KB

Interview Guide - Quant Games 2026 Project

Competition: Quant Games 2026 (FYERS x KSHITIJ)
Result: 🥇 1st Place / 200+ Teams
Purpose: Interview preparation for quantitative trading roles


Introduction

This guide prepares you to discuss your championship-winning Quant Games 2026 project in technical interviews at quantitative trading firms, investment banks, and hedge funds. It covers:

  • Project overview (elevator pitch with championship highlights)
  • Technical deep-dives
  • Strategy explanations
  • Common interview questions & answers
  • Problem-solving demonstrations
  • Code walkthroughs

Table of Contents

  1. 🏆 The Championship Story (MUST READ FIRST)
  2. The Elevator Pitch
  3. Project Overview Questions
  4. Strategy & Algorithm Questions
  5. Technical Implementation Questions
  6. Risk Management Questions
  7. Optimization & Machine Learning Questions
  8. Market Microstructure Questions
  9. Behavioral Questions
  10. Code Walkthrough Examples
  11. Whiteboard Problem Examples

🏆 The Championship Story

Q1: Tell me about your Quant Games win. (MOST IMPORTANT QUESTION)

30-Second Version: "I won 1st place out of 200+ teams in Quant Games 2026 with a 2.276 portfolio Sharpe ratio. My key innovation was a novel RSI boosting technique that improved strategy performance by up to 1,120% by filtering false signals through a simple +3-4 point entry delay. I also debugged three critical bugs in the final hour before deadline, demonstrating crisis engineering skills under extreme time pressure."

2-Minute Version: "I competed solo in Competition's Quant Games 2026—a 36-hour algorithmic trading competition with 200+ teams from top IITs. The challenge: develop profitable trading strategies for 5 Indian market symbols with strict compliance rules.

I won 1st place with a 2.276 portfolio Sharpe ratio. My winning approach had three key elements:

First, symbol-specific strategy design—I recognized that indices behave differently from equities. NIFTY50 got a trend-following strategy with momentum ladders, while SUNPHARMA and RELIANCE used mean-reversion with RSI(2).

Second, my breakthrough innovation: RSI boosting. Traditional RSI mean-reversion enters at RSI < 30. I discovered that delaying entry to RSI < 34-41 filters 40% of false signals while keeping 95% of genuine reversals. This simple change improved SUNPHARMA from 3.32 to 4.29 Sharpe—a 29% gain. For YESBANK, it was transformational: 0.14 to 1.76 Sharpe, a 1,120% improvement.

Third, crisis management: With 60 minutes before deadline, my validation tests revealed three critical bugs—capital overflow (PnL = 4.4e+66), YESBANK overfitting (train 2.57 → test -2.2), and VBL trade count violation. I debugged all three under extreme pressure and prevented disqualification.

The win validated my systematic optimization approach and crisis engineering skills—both critical for quant trading."

5-Minute Version: [Include full crisis story, specific examples of each bug fix, walk through RSI boosting discovery process, explain validation methodology, discuss what separated 1st place from 2nd-5th place teams, emphasize the 4.292 SUNPHARMA Sharpe as best-in-competition]


The Elevator Pitch

30-Second Version (Updated)

"I won 1st place in Competition's Quant Games 2026 (200+ teams) with a 2.276 Portfolio Sharpe Ratio. I developed symbol-specific strategies—trend-following for indices, mean-reversion for large-caps—and discovered a novel 'RSI Boosting' technique that improved performance by 29-1,120%. My best strategy achieved 4.292 Sharpe on SUNPHARMA, the highest in the competition. I also debugged three critical bugs in the final hour, demonstrating crisis engineering under pressure."

2-Minute Version (Updated)

"I competed in Competition's Quant Games 2026—a 36-hour quantitative trading competition with 200+ teams from top IITs. I won 1st place with a 2.276 portfolio Sharpe ratio.

The challenge: develop profitable strategies for 5 Indian market symbols (NIFTY50, RELIANCE, SUNPHARMA, VBL, YESBANK) with minimum 120 trades per symbol and strict position sizing rules.

The breakthrough came with my 'RSI Boosting' innovation - by shifting RSI thresholds by 3-4 points, I captured trades earlier in the mean reversion cycle, increasing Sharpe by 20-30% without degrading win rates. This took SUNPHARMA from 2.9 to 4.3 Sharpe.

I used Optuna's Bayesian optimization for hyperparameter tuning (2,500+ trials), walk-forward validation to prevent over-fitting, and rigorous transaction cost modeling. Final validated Sharpe was 2.276 - enough for an expected top 3-5 finish."


Project Overview Questions

Q1: "Walk me through your project."

Answer Structure:

  1. Context: Quant Games 2026 competition, 5 symbols, intraday trading
  2. Challenge: Achieve high Sharpe while meeting 120 trade minimum per symbol
  3. Approach: Symbol-specific strategies, rigorous optimization, innovation
  4. Results: 2.276 Portfolio Sharpe, top 3-5 ranking
  5. Key Learning: Systematic approach + innovation beats complexity

Detailed Response:

"This was a 6-week algorithmic trading competition where teams developed intraday strategies for 5 Indian market symbols. The scoring metric was portfolio-level Sharpe ratio, with strict rules: minimum 120 trades per symbol, 20% max capital per trade, no overnight positions.

I took a systematic four-phase approach:

Phase 1: Baseline strategies (achieved 0.8 Sharpe) - Simple RSI(14) mean reversion

Phase 2: Symbol-specific strategies (1.45 Sharpe) - Recognized different symbols need different approaches. Trend-following for NIFTY50, fast mean reversion for stocks

Phase 3: Hyperparameter optimization (2.12 Sharpe) - Used Optuna to systematically optimize 15-20 parameters per strategy across 2,500 trials

Phase 4: RSI Boosting innovation (2.56 Sharpe) - Discovered that artificially shifting RSI by 3-4 points captures mean reversion earlier, dramatically improving results

After walk-forward validation and transaction cost adjustments, my final submitted Sharpe was 2.276, placing me in the expected top 3-5 out of 100+ participating teams."

Q2: "What was your biggest challenge?"

Answer:

"The biggest challenge was balancing three competing objectives:

1. Meeting minimum trade requirements (120+ per symbol) 2. Maintaining high win rates and Sharpe ratios 3. Avoiding over-optimization on limited historical data

For example, NIFTY50 and YESBANK struggled to reach 120 trades with conservative strategies. I solved NIFTY50 by adding volume confirmation and tighter profit targets, increasing trades from 89 to 132 while maintaining Sharpe.

For YESBANK, the extreme volatility made aggressive trading dangerous. I accepted fewer trades (69) with very conservative parameters, prioritizing capital preservation over meeting the minimum.

The over-optimization challenge was addressed through walk-forward validation - I divided data into 60% training, 20% validation, 20% test. Parameters were frozen after training. The test period showed only 8.2% Sharpe decay, confirming robustness."

Q3: "What were your key results?"

Answer:

"Final validated results:

- Portfolio Sharpe: 2.276 (top 3-5 out of 100+ teams) - Total Trades: 757 across 5 symbols - Best Individual Strategy: SUNPHARMA at 4.292 Sharpe - Win Rate: 67.2% average across portfolio - Maximum Drawdown: -8.2% (well-controlled) - Transaction Costs: Fully incorporated (0.08% per trade)

Key achievements: 1. 184% improvement from baseline (0.8 → 2.276 Sharpe) 2. RSI Boosting innovation (+20-30% Sharpe on best strategies) 3. 100% Rule 12 compliance 4. Passed all stress tests (profitable even with 2× costs)

What I'm most proud of: The systematic optimization process. I didn't just stumble upon good parameters - I tested 2,500+ configurations systematically and validated thoroughly."


Strategy & Algorithm Questions

Q4: "Explain your best-performing strategy in detail."

Answer: SUNPHARMA Hybrid Adaptive V2 Boosted (4.292 Sharpe)

"This strategy combines mean reversion with adaptive volatility scaling and an innovative RSI boosting technique.

Core Logic:

1. Fast RSI Signal Generation:

  • Calculate RSI with 2-period lookback (much faster than typical 14-period)*
  • Apply +4 point 'boost' to RSI values*
  • Entry when boosted RSI < 30 (effectively captures RSI 26-30 range)*
  • Exit when boosted RSI > 70 (effectively 66-70 range)*

2. Volatility Adaptation:

  • Calculate 18-period rolling volatility*
  • Scale position size inversely: size = base_size / (1 + 2 × volatility)*
  • Reduces exposure during turbulent periods, increases during stable markets*

3. Risk Controls:

  • Maximum hold time: 11 hours (prevents overnight risk)*
  • Forced exit at 3:25 PM (before market close)*
  • Position size: 78% of capital (optimal from optimization)*

Why It Works:

SUNPHARMA exhibits strong mean reversion characteristics. When RSI drops to oversold levels (below 30), the stock tends to bounce back quickly. The 2-period RSI is fast enough to capture these moves before they exhaust.

The breakthrough was the +4 boost - by capturing RSI in the 26-30 range instead of waiting for strict < 30, we enter slightly earlier in the reversion cycle. Data showed these 'boosted' trades had nearly identical win rates (72.4% vs 71.9%) but dramatically increased trade count (+15%).

Volatility adaptation prevents over-sizing during risky periods. During high-vol regimes, position sizes automatically shrink to 40-50% of base, limiting drawdowns."

Q5: "How does RSI Boosting work mathematically?"

Answer:

"RSI Boosting is an artificial threshold adjustment that captures mean reversion momentum earlier.

Mathematical Formulation:

Traditional RSI entry: RSI(t) < 30

With +4 boost:

RSI_boosted(t) = RSI(t) + 4
Entry Signal = RSI_boosted(t) < 30
Substituting: (RSI(t) + 4) < 30
Simplifying: RSI(t) < 26

So we're effectively lowering the entry threshold from 30 to 26.

But here's the clever part: Instead of changing the threshold (which would alter exit logic symmetry), we add to RSI itself. This maintains the 30/70 thresholds conceptually while accessing earlier trades.

Exit signal:

Exit = RSI_boosted(t) > 70
(RSI(t) + 4) > 70
RSI(t) > 66

So exits happen slightly earlier too (at RSI 66 instead of 70), which actually helps - we take profits before full overbought exhaustion.

Key Insight: The 26-30 RSI range (for entries) represents stocks that are 'significantly oversold but not extreme'. Historical analysis showed this range has 72% win rate - nearly identical to the < 26 'extreme oversold' range. By accessing these trades, we get 15-25% more opportunities without quality degradation."*

Q6: "Why not use machine learning for predictions?"

Answer:

"I actually tested ML approaches but found them inferior to systematic rule-based strategies for this problem. Here's why:

Experiment: Random Forest Price Direction Predictor

Approach:

  • Features: 50+ technical indicators (RSI, MACD, Bollinger Bands, etc.)*
  • Target: Next bar return > 0 (binary classification)*
  • Model: Random Forest with 100 trees*
  • Validation: 5-fold cross-validation*

Results:

  • Training accuracy: 67%*
  • Validation accuracy: 52% (essentially random!)*
  • Backtest Sharpe: 0.35 (terrible)*

Why ML Failed:

1. Market Noise: Intraday price movements are ~80% noise, ~20% signal. ML models overfit to noise 2. Non-Stationarity: Market regimes change. Models trained on one period fail on another 3. Limited Data: 2 years of 5-minute bars isn't enough for complex models 4. Feature Engineering Trap: Adding more indicators doesn't improve signal-to-noise

Where ML DID Help:

I used Optuna (Bayesian optimization with TPE algorithm) for hyperparameter tuning. This is ML applied to the optimization problem, not prediction:

  • Learns which parameter combinations are promising*
  • Converges in 500 trials vs 10,000+ for grid search*
  • Improved Sharpe by 46% over manual tuning*

Lesson: For high-noise environments like intraday trading, simple rule-based strategies with systematic optimization outperform complex black-box ML models. ML is better suited for parameter optimization, not direct prediction."*

Q7: "How did you handle different market regimes?"

Answer:

"I used two approaches depending on the symbol:

1. Implicit Regime Adaptation (RELIANCE, SUNPHARMA):

The Hybrid Adaptive V2 strategy doesn't explicitly detect regimes but adapts through volatility-based position sizing:

current_vol = returns.rolling(15).std() * np.sqrt(252)
position_size = base_size / (1 + 2 * current_vol)

When volatility spikes (trending/crisis regime), positions automatically shrink. When volatility is low (mean-reverting regime), positions grow. This works well for moderately volatile stocks.

2. Explicit Regime Switching (VBL):

VBL has extreme volatility (42% annualized) with clear regime changes. I implemented explicit regime detection:

vol_30d = returns.rolling(30).std() * np.sqrt(252)

if vol_30d < 0.18:
    regime = 'LOW_VOL'
    strategy = mean_reversion_aggressive
    rsi_entry, rsi_exit = 25, 75  # Tight bands
    position_size = 0.80
elif vol_30d < 0.35:
    regime = 'MEDIUM_VOL'
    strategy = hybrid_approach
    rsi_entry, rsi_exit = 30, 70  # Standard
    position_size = 0.60
else:
    regime = 'HIGH_VOL'
    strategy = breakout_only
    rsi_entry, rsi_exit = 35, 65  # Wide bands
    position_size = 0.40

Results:

  • VBL spent 35% in low-vol regime (best returns: +0.9% per trade)*
  • 45% in medium-vol (moderate returns: +0.4% per trade)*
  • 20% in high-vol (defensive, small losses: -0.2% per trade)*

The regime switching prevented catastrophic losses during VBL's explosive moves while capturing profits during calm periods."


Technical Implementation Questions

Q8: "Walk me through your code architecture."

Answer:

"I used a modular, object-oriented design with clear separation of concerns:

1. Strategy Layer (/src/strategies/):

class BaseStrategy(ABC):
    @abstractmethod
    def generate_signals(self, data):
        pass
    
    @abstractmethod
    def calculate_position_size(self, data, signal):
        pass

class HybridAdaptiveV2(BaseStrategy):
    def __init__(self, rsi_period, rsi_entry, rsi_exit, rsi_boost, ...):
        self.rsi_period = rsi_period
        self.rsi_boost = rsi_boost
        # Store all parameters
    
    def generate_signals(self, data):
        rsi = self.calculate_rsi(data['close'], self.rsi_period)
        rsi_boosted = rsi + self.rsi_boost
        
        signals = []
        for i in range(len(data)):
            if rsi_boosted[i] < self.rsi_entry:
                signals.append('BUY')
            elif rsi_boosted[i] > self.rsi_exit:
                signals.append('SELL')
            else:
                signals.append('HOLD')
        return signals

2. Backtesting Engine (/src/backtesting/):

class Backtester:
    def __init__(self, initial_capital=100000):
        self.capital = initial_capital
        self.trades = []
        self.positions = []
    
    def run(self, strategy, data):
        signals = strategy.generate_signals(data)
        
        for i in range(len(data)):
            if signals[i] == 'BUY' and not self.has_position():
                self.enter_trade(data, i, strategy)
            elif signals[i] == 'SELL' and self.has_position():
                self.exit_trade(data, i)
        
        return self.calculate_metrics()
    
    def calculate_metrics(self):
        returns = [t.return_pct for t in self.trades]
        sharpe = np.sqrt(252) * np.mean(returns) / np.std(returns)
        # ... other metrics
        return {'sharpe': sharpe, ...}

3. Optimization Framework (/optimization/):

def objective_function(trial, symbol, data):
    # Define parameter search space
    params = {
        'rsi_period': trial.suggest_int('rsi_period', 2, 5),
        'rsi_entry': trial.suggest_int('rsi_entry', 20, 35),
        'rsi_boost': trial.suggest_int('rsi_boost', 0, 6),
        # ... more parameters
    }
    
    # Create strategy with these parameters
    strategy = HybridAdaptiveV2(**params)
    
    # Run backtest
    backtester = Backtester()
    results = backtester.run(strategy, data)
    
    return results['sharpe']

# Optimize
study = optuna.create_study(direction='maximize')
study.optimize(lambda trial: objective_function(trial, 'SUNPHARMA', data),
               n_trials=500)

4. Data Pipeline (/src/data/):

  • Download raw price data from FYERS API*
  • Clean and validate (remove bad ticks, handle gaps)*
  • Feature engineering (add indicators)*
  • Train/validation/test split*

5. Utilities (/src/utils/):

  • Technical indicators (RSI, SMA, Bollinger Bands)*
  • Performance metrics calculation*
  • Plotting and visualization*
  • Transaction cost calculations*

Key Design Principles:

  1. Modularity: Each component can be tested independently*
  2. Extensibility: Easy to add new strategies (inherit from BaseStrategy)*
  3. Reproducibility: All random seeds fixed, results deterministic*
  4. Performance: Vectorized operations using NumPy/Pandas (10× faster)*"

Q9: "How did you optimize for performance?"

Answer:

"Several optimization techniques:

1. Vectorization (NumPy/Pandas):

Slow (loop-based):

rsi = []
for i in range(len(prices)):
    rsi.append(calculate_rsi_single(prices[:i+1]))  # Recalculates each time
# Time: 45 seconds for 10,000 bars

Fast (vectorized):

gains = prices.diff().clip(lower=0)
losses = -prices.diff().clip(upper=0)
avg_gains = gains.rolling(period).mean()
avg_losses = losses.rolling(period).mean()
rs = avg_gains / avg_losses
rsi = 100 - (100 / (1 + rs))
# Time: 0.2 seconds for 10,000 bars (225× faster!)

2. Parallel Optimization:

Optuna trials are independent and embarrassingly parallel. I ran optimization on 4 machines simultaneously:

study = optuna.create_study(
    storage='postgresql://...',  # Shared database
    direction='maximize'
)

# Each machine runs this:
study.optimize(objective, n_trials=125)  # 4 × 125 = 500 total

This reduced optimization time from 87 hours to 22 hours (4× speedup).

3. Caching Indicator Calculations:

from functools import lru_cache

@lru_cache(maxsize=128)
def calculate_rsi(prices_hash, period):
    # Expensive calculation done once per unique input
    return rsi_values

# Hash prices for caching
prices_hash = hash(tuple(prices))
rsi = calculate_rsi(prices_hash, period)

4. Data Structure Optimization:

Used Pandas for data manipulation but NumPy arrays for computation-heavy loops:

# Pandas (flexible but slower)
signals_df = data.apply(lambda row: generate_signal(row), axis=1)

# NumPy (less flexible but 50× faster)
prices_array = data['close'].values
signals_array = np.where(rsi_array < 30, 1, 0)

Results:

  • Full backtest (5 symbols, 2 years): 18 seconds (was 15 minutes)*
  • Single optimization trial: 2.3 seconds (was 40 seconds)*
  • Total optimization (2,500 trials): 22 hours (was 87 hours)*"

Risk Management Questions

Q10: "How did you manage risk?"

Answer:

"Multi-layered risk management:

1. Position Sizing:

def calculate_position_size(capital, volatility, base_size=0.70):
    # Reduce size when volatility is high
    vol_adjusted_size = base_size / (1 + 2 * volatility)
    
    # Never exceed 20% (Rule 12 compliance)
    max_size = 0.20
    
    # Never use more than 80% of capital (reserve buffer)
    practical_max = 0.80
    
    return min(vol_adjusted_size, max_size, practical_max)

2. Time-Based Exits:

Maximum hold times prevent getting stuck in losing trades:

  • SUNPHARMA/RELIANCE: 10-11 hours*
  • VBL: 6-8 hours (higher volatility = shorter holds)*
  • YESBANK: 8 hours (high risk)*

3. End-of-Day Exit:

All positions forcefully closed at 3:25 PM (5 minutes before close):

if current_time >= pd.Timestamp('15:25:00').time():
    if self.has_position():
        self.force_exit(reason='EOD_CLOSE')

This prevents overnight gap risk (Rule 12 requirement).

4. Portfolio-Level Diversification:

5 symbols across different sectors:

  • Index (NIFTY50)*
  • Oil & Gas (RELIANCE)*
  • Pharmaceuticals (SUNPHARMA)*
  • Beverage (VBL)*
  • Banking (YESBANK)*

Correlation between symbols: 0.3-0.5 (moderate diversification)

5. Drawdown Monitoring:

Tracked maximum drawdown in real-time:

def update_drawdown(self, current_equity):
    self.peak_equity = max(self.peak_equity, current_equity)
    drawdown = (current_equity - self.peak_equity) / self.peak_equity
    
    if drawdown < -0.15:  # 15% drawdown threshold
        self.trigger_risk_alert()

6. Regime-Aware Adjustment (VBL):

Automatically reduces position sizes by 50% in high-volatility regimes.

Results:

  • Maximum portfolio drawdown: -8.2% (excellent control)*
  • No single trade lost more than 1.2% of capital*
  • 95% of trades risked less than 0.3% of capital*"

Optimization & Machine Learning Questions

Q11: "Explain Optuna and why you chose it."

Answer:

"Optuna is a Bayesian hyperparameter optimization framework using the Tree-structured Parzen Estimator (TPE) algorithm.

Why I Chose Optuna:

1. Efficiency: Converges much faster than grid/random search

  • Grid search: Would need 10,000+ trials to cover my parameter space*
  • Random search: Inefficient, doesn't learn from trials*
  • Optuna (TPE): Converges in 500 trials*

2. Mixed Parameter Types:

  • Integers: RSI period (2-5)*
  • Floats: Position size (0.5-0.9)*
  • Categoricals: Strategy type*

3. Easy Pruning:

  • Stop bad trials early (if Sharpe < 0.5 after 100 trades, abort)*
  • Saves computation time*

4. Distributed Optimization:

  • Runs trials across multiple machines with shared database*
  • Crucial for my 2,500-trial optimization*

How TPE Works:

1. Start with random sampling (first 50 trials) 2. Model promising regions using probability distributions:

  • P(parameters | good results)*
  • P(parameters | bad results)* 3. Sample next parameters from regions where:
P(params | good) / P(params | bad) is maximized

4. Update models and repeat

Example:

After 100 trials, Optuna learned:

  • RSI period = 2 appears in 80% of top-10 trials*
  • RSI boost 3-5 correlates with high Sharpe*
  • Volatility window 15-20 is optimal*

Next trials focused on these ranges, avoiding obviously bad regions (e.g., RSI period = 14).

Results:

  • Trial 1-100: Best Sharpe 2.15*
  • Trial 101-300: Best Sharpe 2.67 (TPE learning!)*
  • Trial 301-500: Best Sharpe 2.87 (minor refinement)*

Optuna found near-optimal parameters 5× faster than grid search would have."

Q12: "How did you prevent over-fitting?"

Answer:

"Over-fitting is the #1 killer of algorithmic trading strategies. I used multiple techniques:

1. Walk-Forward Validation (Primary Defense):

Data split:

  • Training: 60% (Jan 2024 - Aug 2025) - Optimize here*
  • Validation: 20% (Sep - Nov 2025) - Verify generalization*
  • Test: 20% (Dec 2025 - Jan 2026) - Final check*

Parameters optimized ONLY on training set. If test Sharpe drops >20%, strategy is over-fitted.

My results:

  • Training: 2.68 Sharpe*
  • Test: 2.46 Sharpe (-8.2%)*
  • Acceptable decay ✅*

2. Parameter Regularization:

Limited parameter ranges to reasonable values:

# Bad: Allows unrealistic values
rsi_period = trial.suggest_int('rsi_period', 1, 50)  # RSI(1) or RSI(50) are nonsense

# Good: Constrained to sensible values
rsi_period = trial.suggest_int('rsi_period', 2, 5)  # Only fast RSI

3. Minimum Trade Count Constraint:

Strategies must generate 120+ trades. This prevents:

# Over-fitted strategy: Only trades once, perfectly times market bottom
if date == '2024-03-15' and time == '10:35':
    buy()  # Buys the exact bottom, 100% win rate, infinite Sharpe!

My strategies trade 130-250 times, ensuring statistical significance.

4. Transaction Cost Reality Check:

Many 'profitable' strategies fail after adding realistic costs (0.08% per trade). This naturally penalizes over-optimized high-frequency strategies.

5. Bootstrap Resampling:

Resampled trades 10,000 times to check if results are stable:

sharpe_distribution = []
for _ in range(10000):
    resampled_trades = random.sample(all_trades, len(all_trades), replace=True)
    sharpe = calculate_sharpe(resampled_trades)
    sharpe_distribution.append(sharpe)

# Check if original Sharpe is within 95% CI
ci_lower, ci_upper = np.percentile(sharpe_distribution, [2.5, 97.5])
# My result: [2.38, 2.74] - original 2.56 is comfortably inside ✅

6. Simplicity Bias:

I explicitly favored simpler strategies:

  • 17+ parameters: Likely over-fitted*
  • 8-12 parameters: Good balance*
  • < 5 parameters: Too simple*

My final strategies: 8-10 parameters each.

7. Out-of-Sample Testing on Live Period:

Tested on Dec 2025 - Jan 2026 (most recent, completely unseen during optimization):

  • Portfolio Sharpe: 2.34 (-8.7% from backtest)*
  • Consistent with validation decay ✅*

Red Flags I Avoided:

  • Sharpe > 5.0 (suspiciously high)*
  • Win rate > 85% (unrealistic)*
  • Perfect equity curve (probably curve-fitted)*
  • Zero-decay on test set (definitely over-fitted)*"

Market Microstructure Questions

Q13: "How did you model transaction costs?"

Answer:

"I implemented a comprehensive transaction cost model including all real-world fees:

def calculate_transaction_costs(trade_value, trade_side):
    \"\"\"
    Calculate all-in transaction costs for Indian equities
    
    Components:
    1. Brokerage: 0.03% (FYERS rate for intraday)
    2. STT (Securities Transaction Tax): 0.025% on sell side only
    3. Exchange charges: 0.00345% (NSE)
    4. SEBI charges: 0.00002%
    5. Stamp duty: 0.003% on buy side
    6. GST: 18% on brokerage + exchange fees
    \"\"\"
    
    brokerage = trade_value * 0.0003
    exchange_fees = trade_value * 0.0000345
    sebi_fees = trade_value * 0.0000002
    
    # GST on brokerage and exchange fees
    gst = (brokerage + exchange_fees) * 0.18
    
    if trade_side == 'SELL':
        stt = trade_value * 0.00025  # Sell side only
        stamp_duty = 0
    else:  # BUY
        stt = 0
        stamp_duty = trade_value * 0.00003  # Buy side only
    
    total_cost = brokerage + stt + exchange_fees + sebi_fees + gst + stamp_duty
    
    return total_cost

# Average round-trip cost: ~0.085% (buy + sell)

Impact Analysis:

Strategy Gross Sharpe Net Sharpe Cost Impact
SUNPHARMA 4.51 4.29 -4.9%
RELIANCE 3.39 3.23 -4.7%
Portfolio 2.68 2.56 -4.5%

Slippage Modeling:

Additional 0.05% slippage per trade (spread crossing + market impact):

def apply_slippage(execution_price, trade_side, slippage_bps=5):
    slippage = execution_price * (slippage_bps / 10000)
    
    if trade_side == 'BUY':
        final_price = execution_price + slippage  # Pay more
    else:  # SELL
        final_price = execution_price - slippage  # Receive less
    
    return final_price

Why This Matters:

Many student projects ignore costs or underestimate them. They might show 3.0 Sharpe on paper, but after real costs, it drops to 0.5. My strategies remain highly profitable (2.56 Sharpe) after realistic costs, proving they would work in live trading."

Q14: "What about market impact and liquidity?"

Answer:

"Excellent question - this is often overlooked by backtests.

Market Impact Modeling:

I assumed square-root impact model (widely used in industry):

def calculate_market_impact(order_size, avg_daily_volume):
    \"\"\"
    Market impact ≈ √(order_size / avg_daily_volume)
    \"\"\"
    participation_rate = order_size / avg_daily_volume
    impact_bps = 10 * np.sqrt(participation_rate)  # In basis points
    
    return impact_bps / 10000  # Convert to decimal

My Position Sizes vs. Liquidity:

Symbol Position Size Avg Daily Vol Participation Impact
NIFTY50 ₹15,000 ₹500 Cr 0.003% 0.5 bps
RELIANCE ₹14,000 ₹300 Cr 0.005% 0.7 bps
SUNPHARMA ₹15,600 ₹180 Cr 0.009% 0.9 bps
VBL ₹12,000 ₹50 Cr 0.024% 1.5 bps
YESBANK ₹11,000 ₹120 Cr 0.009% 0.9 bps

All impacts < 2 bps (0.02%) - negligible for ₹1L portfolio.

But what about scale?

If I scaled to ₹1 Crore (100× size):

  • VBL participation → 2.4% (impact ~15 bps)*
  • This would significantly hurt profitability*

Scalability Analysis:

def max_scalable_capital(symbol, strategy_sharpe, max_acceptable_impact_bps=50):
    \"\"\"
    Calculate maximum capital before market impact destroys profitability
    \"\"\"
    avg_trade_size_pct = 0.15  # 15% of capital per trade
    daily_volume = get_avg_daily_volume(symbol)
    
    # Max order size for acceptable impact
    max_order = (max_acceptable_impact_bps / 10) ** 2 * daily_volume
    
    # Max capital
    max_capital = max_order / avg_trade_size_pct
    
    return max_capital

# Results:
# NIFTY50: ₹50 Cr (highly scalable)
# RELIANCE: ₹30 Cr (very scalable)
# SUNPHARMA: ₹15 Cr (moderately scalable)
# VBL: ₹2 Cr (limited scalability) ⚠️
# YESBANK: ₹5 Cr (limited scalability)

My Portfolio Capacity: ₹2-3 Crores (limited by VBL and YESBANK)

Beyond this, would need to reduce positions in illiquid symbols or find alternative strategies.


Behavioral Questions

Q15: "Tell me about a time you failed."

Answer:

"My biggest failure was wasting two full days developing an options-based hedging strategy - only to discover on day 3 that the competition doesn't allow options trading.

What Happened:

Early in the competition, I noticed my equity strategies had high drawdowns during market crashes. I thought, 'Why not hedge with index put options?' I spent December 20-21 building:

  • Black-Scholes pricing model*
  • Greeks calculation (delta, gamma, vega)*
  • Dynamic hedging algorithm*
  • Backtest showing 30% drawdown reduction*

On December 22, I finally read the detailed competition rules: 'Only equity intraday trading allowed. No derivatives.'

Impact:

Lost 2 days of development time during the critical optimization phase.

What I Learned:

1. Read requirements THOROUGHLY before starting - Not just skim, but understand every constraint 2. Validate assumptions early - Should have confirmed options trading was allowed on day 1 3. Fail fast - When trying something new, check feasibility before deep implementation

How I Recovered:

I re-focused on regime-aware position sizing for VBL, which achieved similar drawdown control without options. The regime-switching strategy reduced VBL drawdowns from -28% to -13%.

This failure taught me a valuable lesson about requirements validation that I now apply to every project - always clarify constraints before deep technical work."

Q16: "Why quantitative trading?"

Answer:

"Three reasons:

1. Intersection of Engineering and Finance:

As a Mechanical Engineering student, I love systems, optimization, and problem-solving. Quantitative trading is the perfect blend - it requires rigorous engineering thinking (algorithms, optimization, testing) applied to financial markets. Building a trading strategy is like designing an engine: both require understanding complex systems, optimizing performance, and rigorous testing.

2. Measurable Performance:

I'm driven by objective metrics. In quant trading, success is clear: Sharpe ratio, returns, drawdowns. There's no ambiguity - either your strategy works or it doesn't. This appeals to my engineering mindset where performance is quantifiable, not subjective.

3. Intellectual Challenge:

Markets are the ultimate adversarial environment. You're competing against:

  • Professional traders with decades of experience*
  • High-frequency firms with microsecond execution*
  • Machine learning systems processing terabytes of data*

The challenge of finding inefficiencies in this environment, and systematically exploiting them, is incredibly engaging. My RSI Boosting innovation - finding that the 26-30 RSI range was under-exploited - exemplifies this: discovering an edge requires both creativity and rigorous analysis.

Long-term Goal:

I want to build systematic trading strategies at a top quantitative fund (Citadel, Two Sigma, DE Shaw). This competition was my proving ground - demonstrating I can develop, optimize, and validate strategies that work in real markets."


Code Walkthrough Examples

Example 1: RSI Calculation

Interviewer: "Show me your RSI implementation."

def calculate_rsi(prices, period=14):
    \"\"\"
    Calculate Relative Strength Index
    
    RSI = 100 - (100 / (1 + RS))
    where RS = Average Gain / Average Loss
    
    Args:
        prices: pandas Series of closing prices
        period: lookback period (typically 14, but I use 2)
    
    Returns:
        pandas Series of RSI values (0-100)
    \"\"\"
    # Calculate price changes
    delta = prices.diff()
    
    # Separate gains and losses
    gains = delta.clip(lower=0)  # Keep only positive changes
    losses = -delta.clip(upper=0)  # Keep only negative changes (make positive)
    
    # Calculate exponential moving averages
    avg_gains = gains.ewm(span=period, adjust=False).mean()
    avg_losses = losses.ewm(span=period, adjust=False).mean()
    
    # Calculate RS and RSI
    rs = avg_gains / avg_losses
    rsi = 100 - (100 / (1 + rs))
    
    return rsi

# Usage in strategy:
rsi = calculate_rsi(data['close'], period=2)
rsi_boosted = rsi + 4  # Apply RSI boost

entry_signal = rsi_boosted < 30
exit_signal = rsi_boosted > 70

Interviewer: "Why exponential moving average instead of simple moving average?"

Answer: "EMA gives more weight to recent prices, making RSI more responsive. For RSI(2) with fast mean reversion, we want quick reaction to oversold/overbought conditions. SMA would lag too much. EMA formula: α = 2/(period+1), so for period=2, α=0.67 - meaning today's price gets 67% weight."


Example 2: Backtesting Engine

Interviewer: "Walk me through your backtesting engine."

class Backtester:
    def __init__(self, initial_capital=100000):
        self.initial_capital = initial_capital
        self.capital = initial_capital
        self.trades = []
        self.current_position = None
        
    def run(self, strategy, data):
        \"\"\"
        Execute backtest for given strategy on data
        \"\"\"
        # Generate signals
        signals = strategy.generate_signals(data)
        
        # Iterate through data
        for i in range(1, len(data)):  # Start at 1 (need previous bar)
            current_time = data.index[i]
            
            # Check for entry signal
            if signals[i] == 'BUY' and self.current_position is None:
                self.enter_position(data, i, strategy, 'LONG')
            
            # Check for exit signal
            elif signals[i] == 'SELL' and self.current_position is not None:
                self.exit_position(data, i)
            
            # Check time-based exits
            elif self.current_position is not None:
                if self.should_exit_time(current_time, self.current_position):
                    self.exit_position(data, i, reason='TIME_EXIT')
                
                # End-of-day forced exit
                if current_time.time() >= pd.Timestamp('15:25:00').time():
                    self.exit_position(data, i, reason='EOD_EXIT')
        
        # Close any remaining positions
        if self.current_position is not None:
            self.exit_position(data, len(data)-1, reason='END_OF_DATA')
        
        return self.calculate_performance_metrics()
    
    def enter_position(self, data, bar_index, strategy, direction):
        \"\"\"Enter a new position\"\"\"
        entry_price = data['open'].iloc[bar_index]  # Next bar open (realistic!)
        
        # Calculate position size
        volatility = self.calculate_volatility(data, bar_index)
        position_size = strategy.calculate_position_size(self.capital, volatility)
        
        # Apply slippage
        entry_price = self.apply_slippage(entry_price, 'BUY')
        
        # Calculate shares
        shares = int((self.capital * position_size) / entry_price)
        
        # Calculate costs
        trade_value = shares * entry_price
        costs = self.calculate_transaction_costs(trade_value, 'BUY')
        
        # Create position
        self.current_position = {
            'entry_time': data.index[bar_index],
            'entry_price': entry_price,
            'shares': shares,
            'direction': direction,
            'entry_costs': costs
        }
        
    def exit_position(self, data, bar_index, reason='SIGNAL'):
        \"\"\"Exit current position\"\"\"
        exit_price = data['open'].iloc[bar_index]  # Next bar open
        exit_price = self.apply_slippage(exit_price, 'SELL')
        
        trade_value = self.current_position['shares'] * exit_price
        exit_costs = self.calculate_transaction_costs(trade_value, 'SELL')
        
        # Calculate P&L
        entry_value = self.current_position['shares'] * self.current_position['entry_price']
        total_costs = self.current_position['entry_costs'] + exit_costs
        pnl = (trade_value - entry_value) - total_costs
        pnl_pct = pnl / entry_value
        
        # Record trade
        self.trades.append({
            'entry_time': self.current_position['entry_time'],
            'exit_time': data.index[bar_index],
            'entry_price': self.current_position['entry_price'],
            'exit_price': exit_price,
            'shares': self.current_position['shares'],
            'pnl': pnl,
            'pnl_pct': pnl_pct,
            'exit_reason': reason
        })
        
        # Update capital
        self.capital += pnl
        
        # Clear position
        self.current_position = None

Interviewer: "Why do you use next bar's open price for entry?"

Answer: "This prevents look-ahead bias. If I generate a BUY signal based on current bar's close, I can only realistically execute on the NEXT bar's open (in live trading). Using current bar's close for entry would mean I'm trading with future information - classic over-fitting trap that fails in live trading."


Whiteboard Problem Examples

Problem 1: Maximum Drawdown

Interviewer: "Write a function to calculate maximum drawdown from an equity curve."

Answer:

def calculate_max_drawdown(equity_curve):
    \"\"\"
    Calculate maximum drawdown from peak equity
    
    Args:
        equity_curve: list or array of equity values over time
    
    Returns:
        max_drawdown: maximum percentage decline from peak (negative value)
        peak_index: index of peak before max drawdown
        trough_index: index of trough (lowest point in max drawdown)
    \"\"\"
    equity = np.array(equity_curve)
    
    # Calculate running maximum (peak equity at each point)
    running_max = np.maximum.accumulate(equity)
    
    # Calculate drawdown at each point
    drawdown = (equity - running_max) / running_max
    
    # Find maximum drawdown
    max_dd_index = np.argmin(drawdown)
    max_drawdown = drawdown[max_dd_index]
    
    # Find peak before max drawdown
    peak_index = np.argmax(equity[:max_dd_index+1])
    
    return max_drawdown, peak_index, max_dd_index

# Example:
equity = [100000, 105000, 103000, 108000, 95000, 98000, 110000]
max_dd, peak_idx, trough_idx = calculate_max_drawdown(equity)
print(f"Max Drawdown: {max_dd:.2%}")  # -12.04%
print(f"Peak at index {peak_idx}: ${equity[peak_idx]}")  # Index 3: $108,000
print(f"Trough at index {trough_idx}: ${equity[trough_idx]}")  # Index 4: $95,000

Follow-up: "What's the time complexity?"

Answer: "O(n) - single pass through the array. We use NumPy's vectorized operations which are highly optimized. Running maximum is O(n), argmin is O(n), total is O(n)."


Problem 2: Sharpe Ratio Calculation

Interviewer: "Calculate annualized Sharpe ratio from daily returns."

Answer:

def calculate_sharpe_ratio(returns, risk_free_rate=0.05, periods_per_year=252):
    \"\"\"
    Calculate annualized Sharpe ratio
    
    Sharpe = (Mean Return - Risk Free Rate) / Std Dev of Returns × √periods
    
    Args:
        returns: array of period returns (e.g., daily)
        risk_free_rate: annual risk-free rate (default 5%)
        periods_per_year: trading periods per year (252 for daily, 52 for weekly)
    
    Returns:
        sharpe_ratio: annualized Sharpe ratio
    \"\"\"
    # Convert annual risk-free rate to period rate
    rf_period = (1 + risk_free_rate) ** (1 / periods_per_year) - 1
    
    # Calculate excess returns
    excess_returns = returns - rf_period
    
    # Calculate mean and std of excess returns
    mean_excess = np.mean(excess_returns)
    std_excess = np.std(excess_returns, ddof=1)  # Sample std dev (ddof=1)
    
    if std_excess == 0:
        return 0  # Avoid division by zero
    
    # Annualize
    sharpe = (mean_excess / std_excess) * np.sqrt(periods_per_year)
    
    return sharpe

# Example:
daily_returns = [0.001, -0.002, 0.003, 0.001, -0.001, 0.004, 0.002]
sharpe = calculate_sharpe_ratio(daily_returns, risk_free_rate=0.05, periods_per_year=252)
print(f"Annualized Sharpe: {sharpe:.2f}")

Interviewer: "Why multiply by square root of periods?"

Answer: "Volatility scales with √time (fundamental property of Brownian motion). If daily std dev is σ, then annual std dev is σ × √252 (assuming 252 trading days). When we annualize Sharpe:

Sharpe_annual = (Mean_daily × 252) / (Std_daily × √252)
             = (Mean_daily / Std_daily) × (252 / √252)
             = Sharpe_daily × √252

This is why we multiply by √periods to annualize."


Closing Questions to Ask Interviewer

Always prepare questions. Here are strong ones:

  1. "What's your firm's approach to alpha discovery - more quantitative research or technological edge?"

  2. "How do you balance strategy complexity with robustness? Do you favor simpler strategies?"

  3. "What's the typical timeline from strategy idea to production deployment?"

  4. "How much autonomy do junior quants have in proposing new strategy ideas?"

  5. "What's your tech stack? Python/C++? Cloud vs on-premise?"

  6. "What percentage of developed strategies actually make it to production?"

  7. "How do you handle strategy decay - how frequently do parameters need re-optimization?"


Summary

This interview guide prepares you to confidently discuss:

✅ Project overview and results (2.276 Sharpe, Top 3-5)
✅ Strategy details (RSI Boosting, Regime Switching)
✅ Technical implementation (Optuna, vectorization)
✅ Risk management (multi-layered approach)
✅ Optimization techniques (walk-forward validation)
✅ Code examples (RSI calculation, backtesting engine)
✅ Problem-solving (maximum drawdown, Sharpe calculation)

Practice each section until you can explain it naturally, with enthusiasm and technical precision. Good luck!


Document Version: 1.0
Last Updated: January 19, 2026