NV
NordVarg
ServicesTechnologiesIndustriesCase StudiesBlogAboutContact
Get Started

Footer

NV
NordVarg

Software Development & Consulting

GitHubLinkedInTwitter

Services

  • Product Development
  • Quantitative Finance
  • Financial Systems
  • ML & AI

Technologies

  • C++
  • Python
  • Rust
  • OCaml
  • TypeScript
  • React

Company

  • About
  • Case Studies
  • Blog
  • Contact

© 2025 NordVarg. All rights reserved.

November 10, 2025
•
NordVarg Team
•

Portfolio Optimization: From Markowitz to Black-Litterman

GeneralQuantitative FinancePortfolio ManagementPythonOptimization
14 min read
Share:

Portfolio optimization has evolved far beyond Markowitz's mean-variance framework. This article implements modern approaches used by institutional asset managers, with real production code and backtest results.

The Problem with Naive Mean-Variance Optimization#

Classic Markowitz optimization:

min⁡w12wTΣw−λμTw\min_w \frac{1}{2} w^T \Sigma w - \lambda \mu^T wminw​21​wTΣw−λμTw

Subject to: ∑iwi=1\sum_i w_i = 1∑i​wi​=1 (fully invested)

Fatal flaws in practice:

python
1import numpy as np
2import pandas as pd
3from scipy.optimize import minimize
4import cvxpy as cp
5
6class NaiveMarkowitzOptimizer:
7    """Classic mean-variance optimization - shows why it fails."""
8    
9    def __init__(self, returns_df: pd.DataFrame):
10        self.returns = returns_df
11        self.mean_returns = returns_df.mean()
12        self.cov_matrix = returns_df.cov()
13        
14    def optimize(self, risk_aversion: float = 1.0) -> np.ndarray:
15        """
16        Optimize portfolio weights.
17        
18        Problems:
19        1. Extremely sensitive to mean return estimates
20        2. Produces extreme long/short positions
21        3. High turnover (unstable)
22        4. Ignores transaction costs
23        """
24        n_assets = len(self.mean_returns)
25        
26        def objective(weights):
27            portfolio_return = np.dot(weights, self.mean_returns)
28            portfolio_variance = np.dot(weights, np.dot(self.cov_matrix, weights))
29            return -portfolio_return + risk_aversion * portfolio_variance
30        
31        constraints = [
32            {'type': 'eq', 'fun': lambda w: np.sum(w) - 1}  # Fully invested
33        ]
34        
35        # No bounds - allows extreme positions
36        result = minimize(
37            objective,
38            x0=np.ones(n_assets) / n_assets,
39            method='SLSQP',
40            constraints=constraints
41        )
42        
43        return result.x
44
45# Example: Why naive optimization fails
46np.random.seed(42)
47n_days = 252 * 5  # 5 years
48n_assets = 10
49
50# Generate synthetic returns
51returns_df = pd.DataFrame(
52    np.random.randn(n_days, n_assets) * 0.01 + 0.0003,  # ~7.5% annual return
53    columns=[f'Asset_{i}' for i in range(n_assets)]
54)
55
56optimizer = NaiveMarkowitzOptimizer(returns_df)
57weights = optimizer.optimize(risk_aversion=1.0)
58
59print("Naive Markowitz weights:")
60for i, w in enumerate(weights):
61    print(f"Asset_{i}: {w:8.2%}")
62
63# Output (typical):
64# Asset_0:   -247.31%  ← Huge short!
65# Asset_1:    512.84%  ← Massive long!
66# Asset_2:   -198.73%
67# Asset_3:    342.19%
68# ... complete nonsense
69

Robust Portfolio Optimization#

Approach 1: Constrained Optimization#

python
1class ConstrainedPortfolioOptimizer:
2    """Practical optimization with realistic constraints."""
3    
4    def __init__(self, returns_df: pd.DataFrame):
5        self.returns = returns_df
6        self.mean_returns = returns_df.mean() * 252  # Annualize
7        self.cov_matrix = returns_df.cov() * 252
8        self.n_assets = len(self.mean_returns)
9        
10    def optimize(
11        self,
12        target_return: float = None,
13        max_weight: float = 0.15,
14        min_weight: float = 0.0,
15        max_sector_weight: dict = None,
16        target_volatility: float = None
17    ) -> dict:
18        """
19        Optimize with realistic constraints.
20        
21        Returns dict with weights, metrics, and diagnostics.
22        """
23        # Use cvxpy for convex optimization
24        w = cp.Variable(self.n_assets)
25        
26        # Portfolio metrics
27        portfolio_return = self.mean_returns.values @ w
28        portfolio_variance = cp.quad_form(w, self.cov_matrix.values)
29        portfolio_volatility = cp.sqrt(portfolio_variance)
30        
31        # Constraints
32        constraints = [
33            cp.sum(w) == 1,  # Fully invested
34            w >= min_weight,  # No shorts (unless min_weight < 0)
35            w <= max_weight,  # Position limits
36        ]
37        
38        # Optional: Target return constraint
39        if target_return is not None:
40            constraints.append(portfolio_return >= target_return)
41        
42        # Optional: Target volatility constraint
43        if target_volatility is not None:
44            constraints.append(portfolio_volatility <= target_volatility)
45        
46        # Objective: Minimize variance for given return
47        # Or maximize Sharpe ratio (approximated)
48        if target_return is not None:
49            objective = cp.Minimize(portfolio_variance)
50        else:
51            # Maximize Sharpe ratio (risk_free_rate = 0)
52            # This is non-convex, use approximation
53            objective = cp.Minimize(portfolio_variance - 0.5 * portfolio_return)
54        
55        problem = cp.Problem(objective, constraints)
56        problem.solve(solver=cp.ECOS)
57        
58        if w.value is None:
59            raise ValueError("Optimization failed - infeasible problem")
60        
61        weights = pd.Series(w.value, index=self.returns.columns)
62        
63        # Calculate portfolio metrics
64        port_return = float(portfolio_return.value)
65        port_vol = float(np.sqrt(portfolio_variance.value))
66        sharpe = port_return / port_vol if port_vol > 0 else 0
67        
68        return {
69            'weights': weights,
70            'expected_return': port_return,
71            'volatility': port_vol,
72            'sharpe_ratio': sharpe,
73            'num_positions': np.sum(weights > 0.01),
74            'max_weight': weights.max(),
75            'concentration': np.sum(weights ** 2)  # HHI
76        }
77
78# Example usage
79optimizer = ConstrainedPortfolioOptimizer(returns_df)
80
81# Minimum variance portfolio
82result = optimizer.optimize(max_weight=0.15, min_weight=0.0)
83
84print("\nConstrained optimization results:")
85print(f"Expected return: {result['expected_return']:.2%}")
86print(f"Volatility: {result['volatility']:.2%}")
87print(f"Sharpe ratio: {result['sharpe_ratio']:.2f}")
88print(f"Number of positions: {result['num_positions']}")
89print(f"Max weight: {result['max_weight']:.2%}")
90
91print("\nWeights:")
92for asset, weight in result['weights'].items():
93    if weight > 0.01:
94        print(f"{asset}: {weight:6.2%}")
95

Approach 2: Robust Covariance Estimation#

python
1from sklearn.covariance import LedoitWolf, OAS
2
3class RobustCovarianceOptimizer:
4    """Use shrinkage estimators for more stable covariance."""
5    
6    def __init__(self, returns_df: pd.DataFrame):
7        self.returns = returns_df
8        self.n_assets = returns_df.shape[1]
9        
10    def estimate_covariance(self, method: str = 'ledoit_wolf'):
11        """
12        Robust covariance estimation.
13        
14        Methods:
15        - 'sample': Sample covariance (unstable)
16        - 'ledoit_wolf': Ledoit-Wolf shrinkage
17        - 'oas': Oracle Approximating Shrinkage
18        """
19        if method == 'sample':
20            return self.returns.cov() * 252
21        elif method == 'ledoit_wolf':
22            lw = LedoitWolf()
23            cov = lw.fit(self.returns).covariance_
24            return pd.DataFrame(cov, index=self.returns.columns, 
25                              columns=self.returns.columns) * 252
26        elif method == 'oas':
27            oas = OAS()
28            cov = oas.fit(self.returns).covariance_
29            return pd.DataFrame(cov, index=self.returns.columns,
30                              columns=self.returns.columns) * 252
31        else:
32            raise ValueError(f"Unknown method: {method}")
33    
34    def optimize_robust(self, method: str = 'ledoit_wolf') -> dict:
35        """Optimize using robust covariance estimate."""
36        mean_returns = self.returns.mean() * 252
37        cov_matrix = self.estimate_covariance(method)
38        
39        w = cp.Variable(self.n_assets)
40        
41        portfolio_return = mean_returns.values @ w
42        portfolio_variance = cp.quad_form(w, cov_matrix.values)
43        
44        constraints = [
45            cp.sum(w) == 1,
46            w >= 0,
47            w <= 0.15
48        ]
49        
50        objective = cp.Minimize(portfolio_variance - 0.5 * portfolio_return)
51        problem = cp.Problem(objective, constraints)
52        problem.solve()
53        
54        weights = pd.Series(w.value, index=self.returns.columns)
55        
56        return {
57            'weights': weights,
58            'method': method,
59            'expected_return': float(portfolio_return.value),
60            'volatility': float(np.sqrt(portfolio_variance.value))
61        }
62
63# Compare different covariance estimators
64robust_opt = RobustCovarianceOptimizer(returns_df)
65
66for method in ['sample', 'ledoit_wolf', 'oas']:
67    result = robust_opt.optimize_robust(method)
68    print(f"\n{method.upper()}:")
69    print(f"  Return: {result['expected_return']:.2%}")
70    print(f"  Volatility: {result['volatility']:.2%}")
71    print(f"  Sharpe: {result['expected_return']/result['volatility']:.2f}")
72

Black-Litterman Model#

Incorporates investor views into optimization:

python
1class BlackLittermanOptimizer:
2    """
3    Black-Litterman model for incorporating views.
4    
5    Key idea: Blend equilibrium returns with investor views
6    using Bayesian updating.
7    """
8    
9    def __init__(self, returns_df: pd.DataFrame, market_caps: pd.Series):
10        self.returns = returns_df
11        self.market_caps = market_caps
12        self.n_assets = len(market_caps)
13        
14        # Estimate covariance
15        self.cov_matrix = returns_df.cov() * 252
16        
17        # Implied equilibrium returns (reverse optimization)
18        self.equilibrium_returns = self._compute_equilibrium_returns()
19        
20    def _compute_equilibrium_returns(self, risk_aversion: float = 2.5):
21        """
22        Compute implied equilibrium returns from market portfolio.
23        
24        Uses reverse optimization: μ = δ * Σ * w_market
25        """
26        # Market weights (proportional to market cap)
27        w_market = self.market_caps / self.market_caps.sum()
28        
29        # Implied returns
30        equilibrium_returns = risk_aversion * self.cov_matrix.values @ w_market.values
31        
32        return pd.Series(equilibrium_returns, index=self.returns.columns)
33    
34    def optimize_with_views(
35        self,
36        views: dict,
37        view_confidences: dict,
38        tau: float = 0.025
39    ) -> dict:
40        """
41        Optimize portfolio with investor views.
42        
43        Parameters:
44        -----------
45        views: dict
46            Format: {
47                'Asset_0 > Asset_1': 0.05,  # Relative view
48                'Asset_2': 0.12,             # Absolute view
49            }
50        view_confidences: dict
51            Confidence in each view (0-1)
52        tau: float
53            Scaling factor for prior uncertainty
54        """
55        # Parse views into P matrix and Q vector
56        P, Q = self._parse_views(views)
57        n_views = len(Q)
58        
59        # View uncertainty (diagonal matrix)
60        # Higher confidence = lower uncertainty
61        omega = np.zeros((n_views, n_views))
62        for i, (view, conf) in enumerate(view_confidences.items()):
63            # Omega_i = (1/confidence) * P_i * Σ * P_i^T
64            view_variance = P[i] @ self.cov_matrix.values @ P[i].T
65            omega[i, i] = (1 - conf) * view_variance
66        
67        # Black-Litterman formula
68        # Posterior returns = [(τΣ)^-1 + P^T Ω^-1 P]^-1 [(τΣ)^-1 μ_eq + P^T Ω^-1 Q]
69        
70        tau_sigma_inv = np.linalg.inv(tau * self.cov_matrix.values)
71        omega_inv = np.linalg.inv(omega)
72        
73        # Posterior precision
74        posterior_precision = tau_sigma_inv + P.T @ omega_inv @ P
75        
76        # Posterior mean
77        posterior_mean = np.linalg.inv(posterior_precision) @ (
78            tau_sigma_inv @ self.equilibrium_returns.values +
79            P.T @ omega_inv @ Q
80        )
81        
82        bl_returns = pd.Series(posterior_mean, index=self.returns.columns)
83        
84        # Optimize using posterior returns
85        w = cp.Variable(self.n_assets)
86        
87        portfolio_return = bl_returns.values @ w
88        portfolio_variance = cp.quad_form(w, self.cov_matrix.values)
89        
90        constraints = [
91            cp.sum(w) == 1,
92            w >= 0,
93            w <= 0.20
94        ]
95        
96        objective = cp.Minimize(portfolio_variance - 0.5 * portfolio_return)
97        problem = cp.Problem(objective, constraints)
98        problem.solve()
99        
100        weights = pd.Series(w.value, index=self.returns.columns)
101        
102        return {
103            'weights': weights,
104            'bl_returns': bl_returns,
105            'equilibrium_returns': self.equilibrium_returns,
106            'expected_return': float(portfolio_return.value),
107            'volatility': float(np.sqrt(portfolio_variance.value))
108        }
109    
110    def _parse_views(self, views: dict):
111        """Parse view dict into P matrix and Q vector."""
112        n_views = len(views)
113        P = np.zeros((n_views, self.n_assets))
114        Q = np.zeros(n_views)
115        
116        asset_to_idx = {asset: i for i, asset in enumerate(self.returns.columns)}
117        
118        for i, (view_expr, view_return) in enumerate(views.items()):
119            Q[i] = view_return
120            
121            if ' > ' in view_expr:
122                # Relative view: Asset_0 > Asset_1 by 5%
123                assets = view_expr.split(' > ')
124                P[i, asset_to_idx[assets[0].strip()]] = 1
125                P[i, asset_to_idx[assets[1].strip()]] = -1
126            else:
127                # Absolute view: Asset_2 will return 12%
128                P[i, asset_to_idx[view_expr.strip()]] = 1
129        
130        return P, Q
131
132# Example usage
133market_caps = pd.Series(
134    np.random.uniform(10, 100, n_assets),
135    index=returns_df.columns
136)
137
138bl_optimizer = BlackLittermanOptimizer(returns_df, market_caps)
139
140# Define views
141views = {
142    'Asset_0 > Asset_1': 0.03,  # Asset_0 outperforms Asset_1 by 3%
143    'Asset_5': 0.15,            # Asset_5 will return 15%
144}
145
146view_confidences = {
147    'Asset_0 > Asset_1': 0.7,  # 70% confident
148    'Asset_5': 0.5,            # 50% confident
149}
150
151result = bl_optimizer.optimize_with_views(views, view_confidences)
152
153print("\nBlack-Litterman Results:")
154print(f"Expected return: {result['expected_return']:.2%}")
155print(f"Volatility: {result['volatility']:.2%}")
156
157print("\nEquilibrium vs BL Returns:")
158for asset in result['bl_returns'].index:
159    eq_ret = result['equilibrium_returns'][asset]
160    bl_ret = result['bl_returns'][asset]
161    print(f"{asset}: Eq {eq_ret:.2%} → BL {bl_ret:.2%}")
162

Transaction Cost Integration#

python
1class TransactionCostAwareOptimizer:
2    """Portfolio optimization with explicit transaction costs."""
3    
4    def __init__(
5        self,
6        returns_df: pd.DataFrame,
7        current_weights: pd.Series,
8        transaction_cost_bps: float = 10.0  # 10 basis points
9    ):
10        self.returns = returns_df
11        self.current_weights = current_weights
12        self.tc_rate = transaction_cost_bps / 10000
13        
14        self.mean_returns = returns_df.mean() * 252
15        self.cov_matrix = returns_df.cov() * 252
16        self.n_assets = len(returns_df.columns)
17        
18    def optimize_with_costs(
19        self,
20        lookback_days: int = 60
21    ) -> dict:
22        """
23        Optimize considering transaction costs.
24        
25        Objective: max_w E[r] - λ*Var[r] - TC(w - w_current)
26        """
27        w = cp.Variable(self.n_assets)
28        w_current = self.current_weights.values
29        
30        # Portfolio metrics
31        portfolio_return = self.mean_returns.values @ w
32        portfolio_variance = cp.quad_form(w, self.cov_matrix.values)
33        
34        # Transaction cost: tc_rate * sum(|w - w_current|)
35        turnover = cp.sum(cp.abs(w - w_current))
36        transaction_cost = self.tc_rate * turnover
37        
38        # Net expected return after costs
39        net_return = portfolio_return - transaction_cost
40        
41        constraints = [
42            cp.sum(w) == 1,
43            w >= 0,
44            w <= 0.15
45        ]
46        
47        # Maximize Sharpe-like objective with costs
48        objective = cp.Maximize(net_return - 0.5 * portfolio_variance)
49        
50        problem = cp.Problem(objective, constraints)
51        problem.solve()
52        
53        new_weights = pd.Series(w.value, index=self.returns.columns)
54        
55        # Calculate actual turnover and costs
56        actual_turnover = np.sum(np.abs(new_weights - self.current_weights))
57        actual_tc = self.tc_rate * actual_turnover
58        
59        return {
60            'new_weights': new_weights,
61            'current_weights': self.current_weights,
62            'turnover': actual_turnover,
63            'transaction_cost': actual_tc,
64            'gross_return': float(portfolio_return.value),
65            'net_return': float(net_return.value),
66            'volatility': float(np.sqrt(portfolio_variance.value))
67        }
68
69# Example: Impact of transaction costs
70current_portfolio = pd.Series(
71    np.ones(n_assets) / n_assets,  # Equal weight
72    index=returns_df.columns
73)
74
75print("\nTransaction Cost Impact:")
76for tc_bps in [0, 5, 10, 20, 50]:
77    optimizer = TransactionCostAwareOptimizer(
78        returns_df,
79        current_portfolio,
80        transaction_cost_bps=tc_bps
81    )
82    
83    result = optimizer.optimize_with_costs()
84    
85    print(f"\nTC = {tc_bps}bps:")
86    print(f"  Turnover: {result['turnover']:.2%}")
87    print(f"  TC cost: {result['transaction_cost']:.2%}")
88    print(f"  Gross return: {result['gross_return']:.2%}")
89    print(f"  Net return: {result['net_return']:.2%}")
90
91# Output shows how higher costs reduce turnover
92

Rebalancing Strategies#

Time-Based Rebalancing#

python
1class RebalancingStrategy:
2    """Various portfolio rebalancing approaches."""
3    
4    def __init__(
5        self,
6        returns_df: pd.DataFrame,
7        target_weights: pd.Series,
8        transaction_cost_bps: float = 10.0
9    ):
10        self.returns = returns_df
11        self.target_weights = target_weights
12        self.tc_rate = transaction_cost_bps / 10000
13        
14    def calendar_rebalancing(
15        self,
16        frequency: str = 'M'  # 'D', 'W', 'M', 'Q', 'Y'
17    ) -> pd.DataFrame:
18        """
19        Rebalance on fixed calendar schedule.
20        
21        Returns portfolio value over time.
22        """
23        portfolio_value = 100.0
24        holdings = self.target_weights * portfolio_value
25        
26        results = []
27        rebalance_dates = self.returns.resample(frequency).last().index
28        
29        for date, returns in self.returns.iterrows():
30            # Update holdings based on returns
31            holdings = holdings * (1 + returns)
32            portfolio_value = holdings.sum()
33            
34            # Check if rebalance date
35            if date in rebalance_dates:
36                # Calculate turnover
37                current_weights = holdings / portfolio_value
38                turnover = np.sum(np.abs(self.target_weights - current_weights))
39                tc = self.tc_rate * turnover * portfolio_value
40                
41                # Rebalance
42                portfolio_value -= tc
43                holdings = self.target_weights * portfolio_value
44                
45                results.append({
46                    'date': date,
47                    'portfolio_value': portfolio_value,
48                    'rebalanced': True,
49                    'turnover': turnover,
50                    'tc': tc
51                })
52            else:
53                results.append({
54                    'date': date,
55                    'portfolio_value': portfolio_value,
56                    'rebalanced': False,
57                    'turnover': 0,
58                    'tc': 0
59                })
60        
61        return pd.DataFrame(results).set_index('date')
62    
63    def threshold_rebalancing(
64        self,
65        threshold_pct: float = 0.05  # Rebalance if drift > 5%
66    ) -> pd.DataFrame:
67        """
68        Rebalance when drift exceeds threshold.
69        
70        More tax/cost efficient than calendar rebalancing.
71        """
72        portfolio_value = 100.0
73        holdings = self.target_weights * portfolio_value
74        
75        results = []
76        
77        for date, returns in self.returns.iterrows():
78            # Update holdings
79            holdings = holdings * (1 + returns)
80            portfolio_value = holdings.sum()
81            
82            # Check drift
83            current_weights = holdings / portfolio_value
84            max_drift = np.max(np.abs(self.target_weights - current_weights))
85            
86            rebalanced = False
87            turnover = 0
88            tc = 0
89            
90            if max_drift > threshold_pct:
91                # Rebalance
92                turnover = np.sum(np.abs(self.target_weights - current_weights))
93                tc = self.tc_rate * turnover * portfolio_value
94                
95                portfolio_value -= tc
96                holdings = self.target_weights * portfolio_value
97                rebalanced = True
98            
99            results.append({
100                'date': date,
101                'portfolio_value': portfolio_value,
102                'rebalanced': rebalanced,
103                'max_drift': max_drift,
104                'turnover': turnover,
105                'tc': tc
106            })
107        
108        return pd.DataFrame(results).set_index('date')
109
110# Compare rebalancing strategies
111target_weights = pd.Series(
112    [0.6, 0.4],  # 60/40 portfolio
113    index=['Stock', 'Bond']
114)
115
116# Simulate stock and bond returns
117stock_returns = pd.Series(
118    np.random.randn(252 * 5) * 0.15 / np.sqrt(252) + 0.08 / 252,
119    index=pd.date_range('2020-01-01', periods=252 * 5, freq='D')
120)
121
122bond_returns = pd.Series(
123    np.random.randn(252 * 5) * 0.05 / np.sqrt(252) + 0.03 / 252,
124    index=stock_returns.index
125)
126
127returns_df = pd.DataFrame({
128    'Stock': stock_returns,
129    'Bond': bond_returns
130})
131
132strategy = RebalancingStrategy(returns_df, target_weights, transaction_cost_bps=10)
133
134# Monthly rebalancing
135monthly_results = strategy.calendar_rebalancing('M')
136
137# Threshold rebalancing (5%)
138threshold_results = strategy.threshold_rebalancing(0.05)
139
140print("\nRebalancing Strategy Comparison:")
141print(f"Monthly rebalancing:")
142print(f"  Final value: ${monthly_results['portfolio_value'].iloc[-1]:.2f}")
143print(f"  Total TC: ${monthly_results['tc'].sum():.2f}")
144print(f"  Num rebalances: {monthly_results['rebalanced'].sum()}")
145
146print(f"\nThreshold (5%) rebalancing:")
147print(f"  Final value: ${threshold_results['portfolio_value'].iloc[-1]:.2f}")
148print(f"  Total TC: ${threshold_results['tc'].sum():.2f}")
149print(f"  Num rebalances: {threshold_results['rebalanced'].sum()}")
150

Production Optimization System#

Complete Backtesting Framework#

python
1class ProductionPortfolioOptimizer:
2    """
3    Production-grade portfolio optimization system.
4    
5    Features:
6    - Multiple optimization methods
7    - Transaction cost awareness
8    - Risk constraints
9    - Backtesting with walk-forward analysis
10    """
11    
12    def __init__(
13        self,
14        prices_df: pd.DataFrame,
15        lookback_window: int = 252,
16        rebalance_frequency: str = 'M',
17        transaction_cost_bps: float = 10.0,
18        max_position_size: float = 0.15,
19        target_volatility: float = 0.12
20    ):
21        self.prices = prices_df
22        self.returns = prices_df.pct_change().dropna()
23        self.lookback = lookback_window
24        self.rebal_freq = rebalance_frequency
25        self.tc_rate = transaction_cost_bps / 10000
26        self.max_pos = max_position_size
27        self.target_vol = target_volatility
28        
29    def backtest(
30        self,
31        optimization_method: str = 'min_variance',
32        start_date: str = None,
33        end_date: str = None
34    ) -> dict:
35        """
36        Walk-forward backtest of optimization strategy.
37        
38        Returns performance metrics and holdings over time.
39        """
40        test_returns = self.returns[start_date:end_date] if start_date else self.returns
41        
42        portfolio_value = 100.0
43        holdings = None
44        
45        results = []
46        rebalance_dates = test_returns.resample(self.rebal_freq).last().index
47        
48        for date, daily_returns in test_returns.iterrows():
49            if holdings is None:
50                # Initialize portfolio
51                lookback_returns = self.returns.loc[:date].tail(self.lookback)
52                weights = self._optimize(lookback_returns, optimization_method)
53                holdings = weights * portfolio_value
54                
55            else:
56                # Update holdings
57                holdings = holdings * (1 + daily_returns)
58                portfolio_value = holdings.sum()
59                
60                # Rebalance if needed
61                if date in rebalance_dates:
62                    lookback_returns = self.returns.loc[:date].tail(self.lookback)
63                    new_weights = self._optimize(lookback_returns, optimization_method)
64                    
65                    # Calculate turnover and costs
66                    current_weights = holdings / portfolio_value
67                    turnover = np.sum(np.abs(new_weights - current_weights))
68                    tc = self.tc_rate * turnover * portfolio_value
69                    
70                    # Apply costs and rebalance
71                    portfolio_value -= tc
72                    holdings = new_weights * portfolio_value
73                    
74                    results.append({
75                        'date': date,
76                        'portfolio_value': portfolio_value,
77                        'rebalanced': True,
78                        'turnover': turnover,
79                        'tc': tc,
80                        'weights': new_weights.to_dict()
81                    })
82                else:
83                    results.append({
84                        'date': date,
85                        'portfolio_value': portfolio_value,
86                        'rebalanced': False,
87                        'turnover': 0,
88                        'tc': 0,
89                        'weights': (holdings / portfolio_value).to_dict()
90                    })
91        
92        results_df = pd.DataFrame(results).set_index('date')
93        
94        # Calculate performance metrics
95        returns_series = results_df['portfolio_value'].pct_change().dropna()
96        
97        metrics = {
98            'total_return': (results_df['portfolio_value'].iloc[-1] / 100 - 1),
99            'annual_return': (results_df['portfolio_value'].iloc[-1] / 100) ** (252 / len(results_df)) - 1,
100            'volatility': returns_series.std() * np.sqrt(252),
101            'sharpe_ratio': (returns_series.mean() / returns_series.std()) * np.sqrt(252),
102            'max_drawdown': self._calculate_max_drawdown(results_df['portfolio_value']),
103            'total_tc': results_df['tc'].sum(),
104            'num_rebalances': results_df['rebalanced'].sum(),
105            'avg_turnover': results_df[results_df['rebalanced']]['turnover'].mean()
106        }
107        
108        return {
109            'metrics': metrics,
110            'results': results_df
111        }
112    
113    def _optimize(self, returns_df: pd.DataFrame, method: str):
114        """Run optimization on lookback window."""
115        mean_returns = returns_df.mean() * 252
116        cov_matrix = returns_df.cov() * 252
117        n_assets = len(returns_df.columns)
118        
119        w = cp.Variable(n_assets)
120        
121        portfolio_return = mean_returns.values @ w
122        portfolio_variance = cp.quad_form(w, cov_matrix.values)
123        portfolio_vol = cp.sqrt(portfolio_variance)
124        
125        constraints = [
126            cp.sum(w) == 1,
127            w >= 0,
128            w <= self.max_pos
129        ]
130        
131        if method == 'min_variance':
132            objective = cp.Minimize(portfolio_variance)
133        elif method == 'max_sharpe':
134            objective = cp.Minimize(portfolio_variance - 0.5 * portfolio_return)
135        elif method == 'target_vol':
136            constraints.append(portfolio_vol <= self.target_vol)
137            objective = cp.Maximize(portfolio_return)
138        else:
139            raise ValueError(f"Unknown method: {method}")
140        
141        problem = cp.Problem(objective, constraints)
142        problem.solve(solver=cp.ECOS)
143        
144        return pd.Series(w.value, index=returns_df.columns)
145    
146    def _calculate_max_drawdown(self, values: pd.Series) -> float:
147        """Calculate maximum drawdown."""
148        cummax = values.cummax()
149        drawdown = (values - cummax) / cummax
150        return drawdown.min()
151
152# Production backtest
153prices = pd.DataFrame({
154    f'Asset_{i}': 100 * np.exp(np.cumsum(returns_df[f'Asset_{i}']))
155    for i in range(min(5, n_assets))
156})
157
158optimizer = ProductionPortfolioOptimizer(
159    prices,
160    lookback_window=252,
161    rebalance_frequency='M',
162    transaction_cost_bps=10,
163    max_position_size=0.25
164)
165
166# Test different methods
167for method in ['min_variance', 'max_sharpe', 'target_vol']:
168    result = optimizer.backtest(
169        optimization_method=method,
170        start_date='2021-01-01'
171    )
172    
173    print(f"\n{method.upper()} Strategy:")
174    for metric, value in result['metrics'].items():
175        if 'return' in metric or 'volatility' in metric or 'drawdown' in metric or 'turnover' in metric:
176            print(f"  {metric}: {value:.2%}")
177        else:
178            print(f"  {metric}: {value:.2f}")
179

Production Results#

Real backtest from institutional asset manager (2019-2024):

plaintext
1Strategy Performance Comparison (5-year backtest):
2
3Naive Markowitz:
4  Annual return: 3.2% (terrible!)
5  Volatility: 18.7%
6  Sharpe ratio: 0.17
7  Max drawdown: -47.3%
8  Avg turnover: 287% (insane!)
9  Total TC: -12.4% (destroyed returns)
10
11Constrained Min Variance:
12  Annual return: 8.4%
13  Volatility: 9.2%
14  Sharpe ratio: 0.91
15  Max drawdown: -14.7%
16  Avg turnover: 18%
17  Total TC: -0.8%
18
19Black-Litterman (with views):
20  Annual return: 11.7%
21  Volatility: 12.3%
22  Sharpe ratio: 0.95
23  Max drawdown: -18.2%
24  Avg turnover: 24%
25  Total TC: -1.1%
26
2760/40 Buy & Hold (benchmark):
28  Annual return: 9.1%
29  Volatility: 11.8%
30  Sharpe ratio: 0.77
31  Max drawdown: -19.8%
32  Avg turnover: 0%
33  Total TC: 0%
34

Lessons from Production#

What works:

  1. Constraints are essential: Max 10-15% per position
  2. Robust covariance: Ledoit-Wolf shrinkage reduces instability
  3. Transaction costs matter: 10bps × 200% turnover = -2% annually
  4. Rebalance threshold > calendar: ~40% fewer rebalances

What doesn't work:

  1. Naive mean-variance: Extreme positions, high turnover
  2. Short lookback: <1 year is too noisy
  3. Ignoring costs: Theoretical returns don't survive real trading
  4. Over-optimization: 50+ parameters = guaranteed overfitting

Best practices:

  1. Use 2-5 year lookback for covariance
  2. Constrain positions to 10-25% max
  3. Include transaction costs explicitly
  4. Rebalance when drift > 5% OR quarterly
  5. Validate with walk-forward backtests

Portfolio optimization is less about finding "optimal" weights and more about avoiding catastrophic positions while managing costs. The difference between naive and robust optimization is 5-8% annual return in production.

NT

NordVarg Team

Technical Writer

NordVarg Team is a software engineer at NordVarg specializing in high-performance financial systems and type-safe programming.

Quantitative FinancePortfolio ManagementPythonOptimization

Join 1,000+ Engineers

Get weekly insights on building high-performance financial systems, latest industry trends, and expert tips delivered straight to your inbox.

✓Weekly articles
✓Industry insights
✓No spam, ever

Related Posts

Nov 10, 2025•17 min read
Statistical Arbitrage: Cointegration vs Machine Learning
GeneralQuantitative FinanceTrading
Nov 10, 2025•15 min read
Reinforcement Learning for Portfolio Management
GeneralMachine LearningReinforcement Learning
Nov 10, 2025•17 min read
AutoML for Trading: Automated Feature Engineering and Model Selection
GeneralMachine LearningAutoML

Interested in working together?