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 25, 2025
•
NordVarg Team
•

Cross-Asset Arbitrage Strategies

Algorithmic Tradingcross-assetarbitrageconvertible-bondscapital-structurebasis-tradingmulti-assetrelative-value
10 min read
Share:

TL;DR – Cross-asset arbitrage exploits pricing relationships across different asset classes. This guide covers convertible bond arbitrage, capital structure arbitrage, basis trading, and multi-asset signal generation with production implementations.

1. Why Cross-Asset Arbitrage?#

Opportunities arise from:

  • Convertible bonds – Equity-bond mispricing
  • Capital structure – CDS-equity relationships
  • Basis trades – Futures-spot spreads
  • Cross-currency – FX-interest rate parity
  • Commodity spreads – Calendar and crack spreads

Key insight: Different markets price the same risk differently.

2. Convertible Bond Arbitrage#

2.1. Strategy Overview#

Convertible bond: Bond + embedded call option on stock.

Arbitrage: Buy undervalued convertible, hedge equity risk.

P&L sources:

  • Gamma trading (like volatility arb)
  • Credit spread compression
  • Carry (coupon - borrow cost)

2.2 Convertible Bond Pricing#

python
1import numpy as np
2from scipy.stats import norm
3import matplotlib.pyplot as plt
4
5class ConvertibleBond:
6    """Convertible bond pricing and Greeks."""
7    
8    def __init__(self, face_value, coupon_rate, maturity, conversion_ratio,
9                 call_price=None, put_price=None):
10        """
11        Args:
12            face_value: Bond face value
13            coupon_rate: Annual coupon rate
14            maturity: Years to maturity
15            conversion_ratio: Shares per bond
16            call_price: Issuer call price (optional)
17            put_price: Investor put price (optional)
18        """
19        self.face_value = face_value
20        self.coupon_rate = coupon_rate
21        self.maturity = maturity
22        self.conversion_ratio = conversion_ratio
23        self.call_price = call_price
24        self.put_price = put_price
25    
26    def straight_bond_value(self, r, credit_spread):
27        """
28        Value as straight bond (no conversion option).
29        
30        Args:
31            r: Risk-free rate
32            credit_spread: Credit spread over risk-free
33        """
34        ytm = r + credit_spread
35        coupon = self.face_value * self.coupon_rate
36        
37        # PV of coupons
38        pv_coupons = coupon * (1 - (1 + ytm)**(-self.maturity)) / ytm
39        
40        # PV of principal
41        pv_principal = self.face_value / (1 + ytm)**self.maturity
42        
43        return pv_coupons + pv_principal
44    
45    def conversion_value(self, stock_price):
46        """Value if converted to stock immediately."""
47        return self.conversion_ratio * stock_price
48    
49    def option_value(self, stock_price, r, sigma, credit_spread):
50        """
51        Value of embedded call option using Black-Scholes.
52        
53        Simplified: assumes European option at maturity.
54        """
55        # Conversion strike
56        K = self.face_value / self.conversion_ratio
57        
58        # Adjust for dividends (simplified: reduce stock price)
59        dividend_yield = 0.02  # Assume 2% dividend yield
60        S_adj = stock_price * np.exp(-dividend_yield * self.maturity)
61        
62        # Black-Scholes
63        d1 = (np.log(S_adj/K) + (r + 0.5*sigma**2)*self.maturity) / (sigma*np.sqrt(self.maturity))
64        d2 = d1 - sigma*np.sqrt(self.maturity)
65        
66        call_value = (S_adj * norm.cdf(d1) - 
67                     K * np.exp(-r*self.maturity) * norm.cdf(d2))
68        
69        return call_value * self.conversion_ratio
70    
71    def price(self, stock_price, r, sigma, credit_spread):
72        """
73        Total convertible bond price.
74        
75        Price = max(straight_bond_value, conversion_value) + option_time_value
76        """
77        bond_floor = self.straight_bond_value(r, credit_spread)
78        conversion_val = self.conversion_value(stock_price)
79        option_val = self.option_value(stock_price, r, sigma, credit_spread)
80        
81        # Convertible value
82        cb_value = bond_floor + option_val
83        
84        # Apply call/put constraints
85        if self.call_price and cb_value > self.call_price:
86            cb_value = self.call_price
87        if self.put_price and cb_value < self.put_price:
88            cb_value = self.put_price
89        
90        return cb_value
91    
92    def delta(self, stock_price, r, sigma, credit_spread, ds=0.01):
93        """Calculate delta (sensitivity to stock price)."""
94        price_up = self.price(stock_price + ds, r, sigma, credit_spread)
95        price_down = self.price(stock_price - ds, r, sigma, credit_spread)
96        return (price_up - price_down) / (2 * ds)
97    
98    def gamma(self, stock_price, r, sigma, credit_spread, ds=0.01):
99        """Calculate gamma (convexity)."""
100        price_mid = self.price(stock_price, r, sigma, credit_spread)
101        price_up = self.price(stock_price + ds, r, sigma, credit_spread)
102        price_down = self.price(stock_price - ds, r, sigma, credit_spread)
103        return (price_up - 2*price_mid + price_down) / (ds**2)
104
105# Example
106cb = ConvertibleBond(
107    face_value=1000,
108    coupon_rate=0.03,
109    maturity=5,
110    conversion_ratio=20,  # 20 shares per bond
111    call_price=1200,
112    put_price=950
113)
114
115# Parameters
116stock_prices = np.linspace(30, 80, 100)
117r = 0.05
118sigma = 0.30
119credit_spread = 0.02
120
121# Calculate prices and Greeks
122cb_prices = [cb.price(S, r, sigma, credit_spread) for S in stock_prices]
123bond_floors = [cb.straight_bond_value(r, credit_spread) for _ in stock_prices]
124conversion_values = [cb.conversion_value(S) for S in stock_prices]
125deltas = [cb.delta(S, r, sigma, credit_spread) for S in stock_prices]
126
127# Plot
128fig, axes = plt.subplots(2, 1, figsize=(14, 10))
129
130axes[0].plot(stock_prices, cb_prices, 'b-', linewidth=2, label='Convertible Bond')
131axes[0].plot(stock_prices, bond_floors, 'r--', label='Bond Floor')
132axes[0].plot(stock_prices, conversion_values, 'g--', label='Conversion Value')
133axes[0].set_xlabel('Stock Price')
134axes[0].set_ylabel('Value')
135axes[0].set_title('Convertible Bond Pricing')
136axes[0].legend()
137axes[0].grid(True, alpha=0.3)
138
139axes[1].plot(stock_prices, deltas, 'b-', linewidth=2)
140axes[1].set_xlabel('Stock Price')
141axes[1].set_ylabel('Delta')
142axes[1].set_title('Convertible Bond Delta (Hedge Ratio)')
143axes[1].grid(True, alpha=0.3)
144
145plt.tight_layout()
146plt.show()
147

2.3 Convertible Arbitrage Strategy#

python
1class ConvertibleArbitrage:
2    """
3    Convertible bond arbitrage strategy.
4    
5    Long convertible bond, short delta-hedged stock.
6    """
7    
8    def __init__(self, cb, initial_stock_price, r, sigma, credit_spread):
9        self.cb = cb
10        self.stock_price = initial_stock_price
11        self.r = r
12        self.sigma = sigma
13        self.credit_spread = credit_spread
14        
15        # Initial positions
16        self.cb_position = 1  # Long 1 bond
17        self.cb_price = cb.price(initial_stock_price, r, sigma, credit_spread)
18        
19        # Delta hedge
20        self.delta = cb.delta(initial_stock_price, r, sigma, credit_spread)
21        self.stock_position = -self.delta * cb.conversion_ratio
22        
23        self.total_pnl = 0
24        self.pnl_history = []
25    
26    def update(self, new_stock_price, rehedge=True):
27        """
28        Update positions with new stock price.
29        
30        Args:
31            new_stock_price: New stock price
32            rehedge: Whether to rehedge delta
33        """
34        # Calculate new CB price
35        new_cb_price = self.cb.price(new_stock_price, self.r, self.sigma, self.credit_spread)
36        
37        # P&L from CB
38        cb_pnl = (new_cb_price - self.cb_price) * self.cb_position
39        
40        # P&L from stock hedge
41        stock_pnl = (new_stock_price - self.stock_price) * self.stock_position
42        
43        # Total P&L
44        daily_pnl = cb_pnl + stock_pnl
45        self.total_pnl += daily_pnl
46        
47        # Rehedge if needed
48        rehedge_cost = 0
49        if rehedge:
50            new_delta = self.cb.delta(new_stock_price, self.r, self.sigma, self.credit_spread)
51            target_stock_position = -new_delta * self.cb.conversion_ratio
52            rehedge_quantity = target_stock_position - self.stock_position
53            
54            # Transaction cost
55            rehedge_cost = abs(rehedge_quantity) * 0.01
56            self.total_pnl -= rehedge_cost
57            
58            self.stock_position = target_stock_position
59            self.delta = new_delta
60        
61        # Update state
62        self.stock_price = new_stock_price
63        self.cb_price = new_cb_price
64        self.pnl_history.append(self.total_pnl)
65        
66        return {
67            'daily_pnl': daily_pnl,
68            'total_pnl': self.total_pnl,
69            'rehedge_cost': rehedge_cost
70        }
71
72# Simulate
73np.random.seed(42)
74n_days = 252
75S0 = 50
76
77# Generate stock price path
78returns = np.random.normal(0, 0.02, n_days)
79stock_prices = S0 * np.exp(np.cumsum(returns))
80
81# Run strategy
82strategy = ConvertibleArbitrage(cb, S0, r=0.05, sigma=0.30, credit_spread=0.02)
83
84for S in stock_prices:
85    strategy.update(S, rehedge=True)
86
87# Plot
88plt.figure(figsize=(14, 8))
89
90plt.subplot(2, 1, 1)
91plt.plot(stock_prices)
92plt.ylabel('Stock Price')
93plt.title('Stock Price Path')
94plt.grid(True, alpha=0.3)
95
96plt.subplot(2, 1, 2)
97plt.plot(strategy.pnl_history)
98plt.xlabel('Days')
99plt.ylabel('Cumulative P&L')
100plt.title('Convertible Arbitrage P&L')
101plt.grid(True, alpha=0.3)
102
103plt.tight_layout()
104plt.show()
105
106print("Final P&L: ${:,.2f}".format(strategy.total_pnl))
107

3. Capital Structure Arbitrage#

3.1 CDS-Equity Relationship#

Merton model: Equity is a call option on firm assets.

Relationship: CDS spreads and equity volatility are linked.

Arbitrage: When CDS is cheap relative to equity put options.

python
1class CapitalStructureArbitrage:
2    """
3    Capital structure arbitrage using Merton model.
4    
5    Trade CDS vs equity options based on implied default probability.
6    """
7    
8    @staticmethod
9    def merton_default_probability(S, D, r, sigma, T):
10        """
11        Calculate default probability using Merton model.
12        
13        Args:
14            S: Equity value
15            D: Debt face value
16            r: Risk-free rate
17            sigma: Equity volatility
18            T: Time horizon
19            
20        Returns:
21            Default probability
22        """
23        # Distance to default
24        d2 = (np.log(S/D) + (r - 0.5*sigma**2)*T) / (sigma*np.sqrt(T))
25        
26        # Default probability = N(-d2)
27        default_prob = norm.cdf(-d2)
28        
29        return default_prob
30    
31    @staticmethod
32    def cds_spread_from_default_prob(default_prob, recovery_rate=0.40):
33        """
34        Implied CDS spread from default probability.
35        
36        Simplified: spread ≈ (1 - recovery) * default_prob / (1 - default_prob)
37        """
38        return (1 - recovery_rate) * default_prob / (1 - default_prob)
39    
40    @staticmethod
41    def find_arbitrage(S, D, r, equity_vol, cds_spread, T=1, recovery=0.40):
42        """
43        Find arbitrage opportunity between CDS and equity.
44        
45        Returns:
46            Signal: 'long_cds_short_equity' or 'short_cds_long_equity' or 'none'
47        """
48        # Implied default probability from equity
49        equity_implied_default = CapitalStructureArbitrage.merton_default_probability(
50            S, D, r, equity_vol, T
51        )
52        
53        # Implied CDS spread from equity
54        equity_implied_cds = CapitalStructureArbitrage.cds_spread_from_default_prob(
55            equity_implied_default, recovery
56        )
57        
58        # Compare with market CDS spread
59        spread_diff = cds_spread - equity_implied_cds
60        
61        # Arbitrage signal
62        threshold = 0.005  # 50 bps
63        
64        if spread_diff > threshold:
65            # CDS expensive relative to equity
66            return {
67                'signal': 'short_cds_long_put',
68                'spread_diff': spread_diff,
69                'equity_implied_default': equity_implied_default,
70                'cds_implied_default': cds_spread / (1 - recovery)
71            }
72        elif spread_diff < -threshold:
73            # CDS cheap relative to equity
74            return {
75                'signal': 'long_cds_short_put',
76                'spread_diff': spread_diff,
77                'equity_implied_default': equity_implied_default,
78                'cds_implied_default': cds_spread / (1 - recovery)
79            }
80        else:
81            return {'signal': 'none', 'spread_diff': spread_diff}
82
83# Example
84S = 100  # Equity value
85D = 80   # Debt face value
86r = 0.05
87equity_vol = 0.30
88cds_spread = 0.03  # 300 bps
89
90arb = CapitalStructureArbitrage.find_arbitrage(S, D, r, equity_vol, cds_spread)
91
92print("Arbitrage signal:", arb['signal'])
93print("Spread difference:", arb['spread_diff'])
94if 'equity_implied_default' in arb:
95    print("Equity-implied default prob:", arb['equity_implied_default'])
96    print("CDS-implied default prob:", arb['cds_implied_default'])
97

4. Basis Trading#

4.1 Futures-Spot Arbitrage#

Basis = Futures Price - Spot Price

Fair basis = Spot × (r - dividend_yield) × T

Arbitrage: When actual basis ≠ fair basis.

python
1class BasisTrade:
2    """
3    Futures-spot basis trading.
4    
5    Exploit deviations from cost-of-carry relationship.
6    """
7    
8    def __init__(self, spot_price, futures_price, r, dividend_yield, T):
9        """
10        Args:
11            spot_price: Current spot price
12            futures_price: Current futures price
13            r: Risk-free rate
14            dividend_yield: Dividend yield
15            T: Time to futures expiry (years)
16        """
17        self.spot_price = spot_price
18        self.futures_price = futures_price
19        self.r = r
20        self.dividend_yield = dividend_yield
21        self.T = T
22    
23    def fair_futures_price(self):
24        """Calculate fair futures price using cost-of-carry."""
25        return self.spot_price * np.exp((self.r - self.dividend_yield) * self.T)
26    
27    def basis(self):
28        """Calculate actual basis."""
29        return self.futures_price - self.spot_price
30    
31    def fair_basis(self):
32        """Calculate fair basis."""
33        return self.fair_futures_price() - self.spot_price
34    
35    def mispricing(self):
36        """Calculate mispricing (actual - fair basis)."""
37        return self.basis() - self.fair_basis()
38    
39    def generate_signal(self, threshold=0.01):
40        """
41        Generate trading signal.
42        
43        Args:
44            threshold: Minimum mispricing to trade (as fraction of spot)
45        """
46        mispricing = self.mispricing()
47        mispricing_pct = mispricing / self.spot_price
48        
49        if mispricing_pct > threshold:
50            # Futures overpriced: sell futures, buy spot
51            return {
52                'signal': 'sell_futures_buy_spot',
53                'mispricing': mispricing,
54                'mispricing_pct': mispricing_pct
55            }
56        elif mispricing_pct < -threshold:
57            # Futures underpriced: buy futures, sell spot
58            return {
59                'signal': 'buy_futures_sell_spot',
60                'mispricing': mispricing,
61                'mispricing_pct': mispricing_pct
62            }
63        else:
64            return {'signal': 'none', 'mispricing': mispricing}
65
66# Example
67basis_trade = BasisTrade(
68    spot_price=100,
69    futures_price=102,
70    r=0.05,
71    dividend_yield=0.02,
72    T=0.25  # 3 months
73)
74
75signal = basis_trade.generate_signal(threshold=0.005)
76
77print("Fair futures price:", basis_trade.fair_futures_price())
78print("Actual futures price:", basis_trade.futures_price)
79print("Mispricing:", signal['mispricing'])
80print("Signal:", signal['signal'])
81

5. Multi-Asset Signal Generation#

5.1 Cross-Asset Momentum#

python
1class CrossAssetMomentum:
2    """
3    Momentum strategy across multiple asset classes.
4    
5    Ranks assets by momentum, allocates to top performers.
6    """
7    
8    def __init__(self, lookback=60, n_top=5):
9        self.lookback = lookback
10        self.n_top = n_top
11    
12    def calculate_momentum(self, prices):
13        """
14        Calculate momentum scores for all assets.
15        
16        Args:
17            prices: DataFrame with asset prices
18            
19        Returns:
20            DataFrame with momentum scores
21        """
22        # Calculate returns over lookback period
23        returns = prices.pct_change(self.lookback)
24        
25        # Rank by returns
26        ranks = returns.rank(axis=1, ascending=False)
27        
28        return ranks
29    
30    def generate_weights(self, prices):
31        """
32        Generate portfolio weights.
33        
34        Long top N assets, equal-weighted.
35        """
36        ranks = self.calculate_momentum(prices)
37        
38        weights = pd.DataFrame(0.0, index=prices.index, columns=prices.columns)
39        
40        for idx in ranks.index:
41            top_assets = ranks.loc[idx].nsmallest(self.n_top).index
42            weights.loc[idx, top_assets] = 1.0 / self.n_top
43        
44        return weights
45
46# Example: Multi-asset universe
47np.random.seed(42)
48n_days = 500
49n_assets = 10
50
51# Generate prices for different asset classes
52asset_names = ['Equity1', 'Equity2', 'Bond1', 'Bond2', 'FX1', 'FX2', 
53               'Commodity1', 'Commodity2', 'Crypto1', 'Crypto2']
54
55# Different volatilities for different asset classes
56vols = [0.20, 0.20, 0.05, 0.05, 0.10, 0.10, 0.25, 0.25, 0.50, 0.50]
57
58prices_dict = {}
59for i, asset in enumerate(asset_names):
60    returns = np.random.normal(0.0005, vols[i]/np.sqrt(252), n_days)
61    prices_dict[asset] = 100 * np.exp(np.cumsum(returns))
62
63prices_multi = pd.DataFrame(prices_dict)
64
65# Run strategy
66cross_asset_mom = CrossAssetMomentum(lookback=60, n_top=3)
67weights = cross_asset_mom.generate_weights(prices_multi)
68
69# Calculate portfolio returns
70returns_multi = prices_multi.pct_change()
71portfolio_returns = (weights.shift(1) * returns_multi).sum(axis=1)
72cum_returns = (1 + portfolio_returns).cumprod()
73
74plt.figure(figsize=(14, 6))
75plt.plot(cum_returns)
76plt.xlabel('Days')
77plt.ylabel('Cumulative Return')
78plt.title('Cross-Asset Momentum Strategy')
79plt.grid(True, alpha=0.3)
80plt.show()
81
82print("Final return:", (cum_returns.iloc[-1] - 1) * 100, "%")
83print("Sharpe ratio:", portfolio_returns.mean() / portfolio_returns.std() * np.sqrt(252))
84

6. Production Checklist#

  • Monitor cross-asset correlations – Key for hedging
  • Implement robust pricing models – Convertibles, CDS
  • Track funding costs – Borrow rates, repo
  • Handle corporate actions – Calls, puts, conversions
  • Monitor liquidity – Bid-ask spreads
  • Implement risk limits – Per asset class
  • Track basis convergence – Futures expiry
  • Document relationships – Theoretical vs empirical
  • Add execution optimization – TWAP, VWAP
  • Monitor regulatory changes – Margin requirements

References#

  1. Calamos, N. (2011). Convertible Arbitrage: Insights and Techniques for Successful Hedging. Wiley.
  2. Currie, A. & Morris, J. (2002). And Now for Capital Structure Arbitrage. RISK.
  3. Duarte, J., Longstaff, F., & Yu, F. (2007). Risk and Return in Fixed-Income Arbitrage. Journal of Financial Economics.
  4. Pedersen, L. (2015). Efficiently Inefficient: How Smart Money Invests and Market Prices Are Determined. Princeton.

Cross-asset arbitrage requires deep understanding of pricing relationships across markets. Focus on robust models, monitor funding costs carefully, and implement proper risk limits. Always validate theoretical relationships with empirical data.

NT

NordVarg Team

Technical Writer

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

cross-assetarbitrageconvertible-bondscapital-structurebasis-trading

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 25, 2025•10 min read
Volatility Arbitrage: The VIX Spike That Made $180M
Algorithmic Tradingvolatility-arbitragedispersion-trading
Nov 25, 2025•9 min read
Momentum and Trend Following at Scale
Algorithmic Tradingmomentumtrend-following
Nov 25, 2025•9 min read
Mean Reversion Strategies: From Pairs Trading to Baskets
Algorithmic Tradingmean-reversionpairs-trading

Interested in working together?