Machine Learning for Portfolio Construction: Beyond Traditional Methods
Apply supervised and reinforcement learning algorithms to portfolio optimization, asset allocation, and risk management in Indian markets.
Traditional portfolio theory assumes normal distributions and linear relationships. Reality is messier: fat tails, regime changes, non-linear dependencies. Machine learning handles this complexity naturally, learning patterns from data without restrictive assumptions.
This guide implements ML-powered portfolio construction for Indian markets, from simple classifiers to advanced reinforcement learning agents.
Why ML for Portfolios?
Limitations of Traditional Methods
Mean-Variance Optimization (Markowitz):
- Assumes normal returns (invalid in crashes)
- Sensitive to input estimates
- Ignores transaction costs
- Static allocation
ML Advantages:
- Learns non-linear patterns
- Adapts to regime changes
- Incorporates alternative data
- Dynamic rebalancing
Part 1: Supervised Learning for Asset Selection
import pandas as pd
import numpy as np
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.model_selection import TimeSeriesSplit
from sklearn.metrics import accuracy_score, precision_score, recall_score
import xgboost as xgb
class MLAssetSelector:
"""
Use ML to predict which assets will outperform
"""
def __init__(self, model_type: str = 'random_forest'):
if model_type == 'random_forest':
self.model = RandomForestClassifier(
n_estimators=100,
max_depth=10,
min_samples_split=20,
random_state=42
)
elif model_type == 'xgboost':
self.model = xgb.XGBClassifier(
n_estimators=100,
max_depth=6,
learning_rate=0.1,
random_state=42
)
def create_features(self, stock_data: pd.DataFrame) -> pd.DataFrame:
"""
Engineer features for ML model
Args:
stock_data: DataFrame with OHLCV data
"""
features = pd.DataFrame(index=stock_data.index)
# Price-based features
features['returns_1d'] = stock_data['Close'].pct_change(1)
features['returns_5d'] = stock_data['Close'].pct_change(5)
features['returns_20d'] = stock_data['Close'].pct_change(20)
# Momentum indicators
features['rsi'] = self.calculate_rsi(stock_data['Close'], 14)
features['macd'] = self.calculate_macd(stock_data['Close'])
# Volatility
features['volatility_20d'] = stock_data['Close'].pct_change().rolling(20).std()
# Volume indicators
features['volume_ratio'] = stock_data['Volume'] / stock_data['Volume'].rolling(20).mean()
# Moving averages
features['sma_50'] = stock_data['Close'].rolling(50).mean()
features['sma_200'] = stock_data['Close'].rolling(200).mean()
features['price_to_sma50'] = stock_data['Close'] / features['sma_50']
features['price_to_sma200'] = stock_data['Close'] / features['sma_200']
# Trend strength
features['adx'] = self.calculate_adx(stock_data)
return features.dropna()
def calculate_rsi(self, prices: pd.Series, period: int = 14) -> pd.Series:
"""Calculate RSI"""
delta = prices.diff()
gain = (delta.where(delta > 0, 0)).rolling(window=period).mean()
loss = (-delta.where(delta < 0, 0)).rolling(window=period).mean()
rs = gain / loss
return 100 - (100 / (1 + rs))
def calculate_macd(self, prices: pd.Series) -> pd.Series:
"""Calculate MACD"""
ema_12 = prices.ewm(span=12).mean()
ema_26 = prices.ewm(span=26).mean()
return ema_12 - ema_26
def calculate_adx(self, stock_data: pd.DataFrame, period: int = 14) -> pd.Series:
"""Calculate ADX (simplified)"""
high = stock_data['High']
low = stock_data['Low']
close = stock_data['Close']
tr = pd.DataFrame({
'hl': high - low,
'hc': abs(high - close.shift()),
'lc': abs(low - close.shift())
}).max(axis=1)
atr = tr.rolling(period).mean()
return atr / close * 100
def create_labels(self, prices: pd.Series, forward_period: int = 20) -> pd.Series:
"""
Create binary labels: 1 if stock outperforms in next N days
Args:
forward_period: Days to look forward
"""
future_returns = prices.pct_change(forward_period).shift(-forward_period)
labels = (future_returns > future_returns.median()).astype(int)
return labels
def train_and_evaluate(
self,
features: pd.DataFrame,
labels: pd.Series
) -> Dict:
"""
Train with time series cross-validation
"""
# Time series split
tscv = TimeSeriesSplit(n_splits=5)
results = []
for fold, (train_idx, val_idx) in enumerate(tscv.split(features)):
X_train, X_val = features.iloc[train_idx], features.iloc[val_idx]
y_train, y_val = labels.iloc[train_idx], labels.iloc[val_idx]
# Train
self.model.fit(X_train, y_train)
# Predict
y_pred = self.model.predict(X_val)
# Metrics
accuracy = accuracy_score(y_val, y_pred)
precision = precision_score(y_val, y_pred)
recall = recall_score(y_val, y_pred)
results.append({
'fold': fold + 1,
'accuracy': accuracy,
'precision': precision,
'recall': recall
})
print(f"Fold {fold+1}: Accuracy={accuracy:.3f}, Precision={precision:.3f}, Recall={recall:.3f}")
results_df = pd.DataFrame(results)
return {
'mean_accuracy': results_df['accuracy'].mean(),
'mean_precision': results_df['precision'].mean(),
'mean_recall': results_df['recall'].mean(),
'fold_results': results_df
}
def predict_top_stocks(
self,
features: pd.DataFrame,
n_stocks: int = 20
) -> pd.DataFrame:
"""
Predict probability of outperformance for all stocks
"""
# Predict probabilities
proba = self.model.predict_proba(features)[:, 1]
# Create results dataframe
predictions = pd.DataFrame({
'symbol': features.index,
'outperform_probability': proba
})
# Select top N stocks
top_stocks = predictions.nlargest(n_stocks, 'outperform_probability')
return top_stocks
# Example usage
selector = MLAssetSelector(model_type='random_forest')
# Load stock data (example for one stock)
import yfinance as yf
stock_data = yf.download('RELIANCE.NS', start='2020-01-01', end='2024-12-31')
# Create features and labels
features = selector.create_features(stock_data)
labels = selector.create_labels(stock_data['Close'], forward_period=20)
# Align features and labels
aligned = features.join(labels.rename('label')).dropna()
X = aligned.drop('label', axis=1)
y = aligned['label']
# Train and evaluate
results = selector.train_and_evaluate(X, y)
print("\n" + "="*60)
print("ML MODEL PERFORMANCE")
print("="*60)
print(f"Mean Accuracy: {results['mean_accuracy']:.3f}")
print(f"Mean Precision: {results['mean_precision']:.3f}")
print(f"Mean Recall: {results['mean_recall']:.3f}")
Part 2: Deep Learning for Return Prediction
import tensorflow as tf
from tensorflow import keras
from sklearn.preprocessing import StandardScaler
class DeepPortfolioOptimizer:
"""
Use neural networks to predict returns and construct portfolios
"""
def __init__(self, n_features: int, n_assets: int):
self.n_features = n_features
self.n_assets = n_assets
self.model = self.build_model()
self.scaler = StandardScaler()
def build_model(self):
"""
Build neural network for return prediction
"""
model = keras.Sequential([
keras.layers.Dense(128, activation='relu', input_shape=(self.n_features,)),
keras.layers.Dropout(0.3),
keras.layers.Dense(64, activation='relu'),
keras.layers.Dropout(0.2),
keras.layers.Dense(32, activation='relu'),
keras.layers.Dense(self.n_assets, activation='linear') # Predict returns for all assets
])
model.compile(
optimizer='adam',
loss='mse',
metrics=['mae']
)
return model
def prepare_data(
self,
features: pd.DataFrame,
forward_returns: pd.DataFrame
) -> tuple:
"""
Prepare data for neural network
Args:
features: Features for all assets
forward_returns: Forward N-day returns for all assets
"""
# Scale features
X = self.scaler.fit_transform(features)
y = forward_returns.values
return X, y
def train(
self,
X_train: np.ndarray,
y_train: np.ndarray,
X_val: np.ndarray,
y_val: np.ndarray,
epochs: int = 50
):
"""Train neural network"""
early_stop = keras.callbacks.EarlyStopping(
monitor='val_loss',
patience=10,
restore_best_weights=True
)
history = self.model.fit(
X_train, y_train,
validation_data=(X_val, y_val),
epochs=epochs,
batch_size=32,
callbacks=[early_stop],
verbose=0
)
return history
def predict_returns(self, features: pd.DataFrame) -> np.ndarray:
"""Predict forward returns for all assets"""
X = self.scaler.transform(features)
predicted_returns = self.model.predict(X, verbose=0)
return predicted_returns
def construct_portfolio(
self,
predicted_returns: np.ndarray,
method: str = 'mean_variance'
) -> np.ndarray:
"""
Construct portfolio weights from predicted returns
Args:
method: 'mean_variance', 'risk_parity', 'long_only'
"""
if method == 'long_only':
# Long only: weight proportional to predicted returns (positive only)
returns_positive = np.maximum(predicted_returns[-1], 0)
if returns_positive.sum() == 0:
weights = np.ones(self.n_assets) / self.n_assets
else:
weights = returns_positive / returns_positive.sum()
elif method == 'mean_variance':
# Simplified mean-variance (would need covariance matrix)
returns_positive = np.maximum(predicted_returns[-1], 0)
weights = returns_positive / returns_positive.sum() if returns_positive.sum() > 0 else np.ones(self.n_assets) / self.n_assets
return weights
# Example
n_assets = 10
n_features = 15
dl_optimizer = DeepPortfolioOptimizer(n_features=n_features, n_assets=n_assets)
# Simulated data
X_train = np.random.randn(1000, n_features)
y_train = np.random.randn(1000, n_assets)
X_val = np.random.randn(200, n_features)
y_val = np.random.randn(200, n_assets)
# Train
history = dl_optimizer.train(X_train, y_train, X_val, y_val, epochs=50)
print(f"Training complete. Final val_loss: {history.history['val_loss'][-1]:.4f}")
Part 3: Reinforcement Learning Portfolio Manager
import gym
from gym import spaces
class PortfolioEnv(gym.Env):
"""
Gym environment for portfolio management
Agent learns to allocate capital across assets
"""
def __init__(self, prices: pd.DataFrame, initial_capital: float = 100000):
super(PortfolioEnv, self).__init__()
self.prices = prices
self.returns = prices.pct_change().fillna(0)
self.n_assets = len(prices.columns)
self.initial_capital = initial_capital
# Action space: portfolio weights (sum to 1)
self.action_space = spaces.Box(
low=0, high=1, shape=(self.n_assets,), dtype=np.float32
)
# Observation space: returns, current weights, other features
self.observation_space = spaces.Box(
low=-np.inf, high=np.inf, shape=(self.n_assets * 3,), dtype=np.float32
)
self.reset()
def reset(self):
"""Reset environment"""
self.current_step = 0
self.capital = self.initial_capital
self.portfolio_weights = np.ones(self.n_assets) / self.n_assets
self.portfolio_value_history = [self.capital]
return self._get_observation()
def _get_observation(self):
"""Get current state"""
if self.current_step >= len(self.returns):
return np.zeros(self.n_assets * 3)
# Current returns
current_returns = self.returns.iloc[self.current_step].values
# Past 20-day returns
if self.current_step >= 20:
past_returns = self.returns.iloc[self.current_step-20:self.current_step].mean().values
else:
past_returns = np.zeros(self.n_assets)
# Current weights
weights = self.portfolio_weights
# Concatenate
observation = np.concatenate([current_returns, past_returns, weights])
return observation.astype(np.float32)
def step(self, action):
"""
Take action (set portfolio weights)
Returns: observation, reward, done, info
"""
# Normalize action to sum to 1
action = np.abs(action)
action = action / (action.sum() + 1e-8)
# Calculate transaction costs (0.1% per trade)
turnover = np.abs(action - self.portfolio_weights).sum() / 2
transaction_cost = turnover * 0.001
# Update weights
self.portfolio_weights = action
# Get returns for current step
if self.current_step >= len(self.returns):
return self._get_observation(), 0, True, {}
period_returns = self.returns.iloc[self.current_step].values
# Calculate portfolio return
portfolio_return = np.dot(self.portfolio_weights, period_returns)
portfolio_return -= transaction_cost
# Update capital
self.capital *= (1 + portfolio_return)
self.portfolio_value_history.append(self.capital)
# Reward: portfolio return
reward = portfolio_return
# Move to next step
self.current_step += 1
done = self.current_step >= len(self.returns) - 1
return self._get_observation(), reward, done, {'portfolio_value': self.capital}
def render(self):
"""Visualize portfolio performance"""
print(f"Step: {self.current_step}, Value: ₹{self.capital:,.0f}")
# Simple DQN Agent
class DQNPortfolioAgent:
"""
Deep Q-Network agent for portfolio management
"""
def __init__(self, state_size: int, action_size: int):
self.state_size = state_size
self.action_size = action_size
self.memory = []
self.gamma = 0.95
self.epsilon = 1.0
self.epsilon_decay = 0.995
self.epsilon_min = 0.01
self.model = self._build_model()
def _build_model(self):
"""Build neural network for Q-learning"""
model = keras.Sequential([
keras.layers.Dense(64, activation='relu', input_shape=(self.state_size,)),
keras.layers.Dense(32, activation='relu'),
keras.layers.Dense(self.action_size, activation='softmax')
])
model.compile(optimizer='adam', loss='mse')
return model
def act(self, state):
"""Choose action"""
if np.random.random() < self.epsilon:
# Random action (exploration)
action = np.random.dirichlet(np.ones(self.action_size))
else:
# Predicted action (exploitation)
action = self.model.predict(state.reshape(1, -1), verbose=0)[0]
return action
def train(self, env: PortfolioEnv, episodes: int = 100):
"""Train agent"""
for episode in range(episodes):
state = env.reset()
total_reward = 0
done = False
while not done:
action = self.act(state)
next_state, reward, done, info = env.step(action)
total_reward += reward
state = next_state
# Decay epsilon
if self.epsilon > self.epsilon_min:
self.epsilon *= self.epsilon_decay
if episode % 10 == 0:
print(f"Episode {episode}, Total Reward: {total_reward:.4f}, "
f"Final Value: ₹{info['portfolio_value']:,.0f}")
# Example usage
# Load historical prices for multiple assets
prices_data = pd.DataFrame({
'RELIANCE': np.random.randn(1000).cumsum() + 2500,
'TCS': np.random.randn(1000).cumsum() + 3500,
'HDFCBANK': np.random.randn(1000).cumsum() + 1600
})
# Create environment
env = PortfolioEnv(prices_data, initial_capital=100000)
# Create and train agent
agent = DQNPortfolioAgent(state_size=env.observation_space.shape[0], action_size=env.n_assets)
agent.train(env, episodes=100)
Conclusion
Machine learning transforms portfolio construction from static rules to adaptive systems:
Key Takeaways:
- ML captures non-linearities traditional methods miss
- Random forests work well for asset selection (60-65% accuracy)
- Deep learning learns complex return patterns
- Reinforcement learning optimizes for long-term wealth
- Combine approaches for robustness
Expected Performance:
- ML-enhanced portfolios: 2-5% alpha over benchmarks
- Sharpe improvement: 0.2-0.4
- Requires: Quality data, computational resources, expertise
ML portfolio management is no longer science fiction—it’s the new standard for serious quantitative traders.
Ready to implement ML portfolios? Contact us for custom ML-powered portfolio systems.