Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions quantstats/reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -1408,7 +1408,7 @@ def metrics(
df["returns"], df["benchmark"], prepare_returns=False
)
metrics["Information Ratio"] = _get_stats().information_ratio(
df["returns"], df["benchmark"], prepare_returns=False
df["returns"], df["benchmark"], compounded=compounded, prepare_returns=False
)
elif isinstance(returns, _pd.DataFrame):
metrics["R^2"] = (
Expand All @@ -1422,7 +1422,10 @@ def metrics(
metrics["Information Ratio"] = (
[
_get_stats().information_ratio(
df[strategy_col], df["benchmark"], prepare_returns=False
df[strategy_col],
df["benchmark"],
compounded=compounded,
prepare_returns=False,
).round(2)
for strategy_col in df_strategy_columns
]
Expand Down
13 changes: 10 additions & 3 deletions quantstats/stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -2632,7 +2632,7 @@ def r2(returns, benchmark):
return r_squared(returns, benchmark)


def information_ratio(returns, benchmark, prepare_returns=True):
def information_ratio(returns, benchmark, compounded=False, prepare_returns=True):
"""
Calculate the Information Ratio.

Expand All @@ -2644,6 +2644,7 @@ def information_ratio(returns, benchmark, prepare_returns=True):
Args:
returns (pd.Series): Return series to analyze
benchmark (pd.Series): Benchmark return series for comparison
compounded (bool): Whether to use geometric mean excess return (default: False)
prepare_returns (bool): Whether to prepare returns first (default: True)

Returns:
Expand All @@ -2662,14 +2663,20 @@ def information_ratio(returns, benchmark, prepare_returns=True):
benchmark = _utils._prepare_benchmark(benchmark, returns.index)

# Calculate active returns (returns - benchmark)
diff_rets = returns - _utils._prepare_benchmark(benchmark, returns.index)
diff_rets = returns - benchmark

# Calculate tracking error (standard deviation of active returns)
std = diff_rets.std()

# Return Information Ratio (active return / tracking error)
if std != 0:
return diff_rets.mean() / diff_rets.std()
if compounded:
active_return = expected_return(returns, compounded=True, prepare_returns=False) - expected_return(
benchmark, compounded=True, prepare_returns=False
)
else:
active_return = diff_rets.mean()
return active_return / std
return 0


Expand Down
11 changes: 11 additions & 0 deletions tests/test_extend_pandas.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,14 @@ def test_cagr_compounded(self, sample_returns):
# May or may not be different depending on returns
assert np.isfinite(result_comp)
assert np.isfinite(result_simple)

def test_information_ratio_compounded(self):
"""Test Information Ratio with compounded option via pandas."""
qs.extend_pandas()
dates = pd.date_range("2020-01-01", periods=4, freq="D")
returns = pd.Series([0.50, -0.20, 0.40, -0.10], index=dates)
benchmark = pd.Series([0.10, 0.05, 0.10, 0.05], index=dates)

result = returns.information_ratio(benchmark, compounded=True, prepare_returns=False)

assert np.isfinite(result)
30 changes: 30 additions & 0 deletions tests/test_stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,36 @@ def test_information_ratio(self, sample_returns, sample_benchmark):
result = stats.information_ratio(sample_returns, sample_benchmark)
assert np.isfinite(result)

def test_information_ratio_compounded(self):
"""Test Information Ratio with compounded active return."""
dates = pd.date_range("2020-01-01", periods=4, freq="D")
returns = pd.Series([0.50, -0.20, 0.40, -0.10], index=dates)
benchmark = pd.Series([0.10, 0.05, 0.10, 0.05], index=dates)

active_returns = returns - benchmark
tracking_error = active_returns.std()
expected = (
stats.expected_return(returns, compounded=True, prepare_returns=False)
- stats.expected_return(benchmark, compounded=True, prepare_returns=False)
) / tracking_error

result = stats.information_ratio(returns, benchmark, compounded=True, prepare_returns=False)

np.testing.assert_allclose(result, expected)

def test_information_ratio_default_matches_arithmetic_mean(self):
"""Test Information Ratio default behavior stays arithmetic."""
dates = pd.date_range("2020-01-01", periods=4, freq="D")
returns = pd.Series([0.50, -0.20, 0.40, -0.10], index=dates)
benchmark = pd.Series([0.10, 0.05, 0.10, 0.05], index=dates)

active_returns = returns - benchmark
expected = active_returns.mean() / active_returns.std()

result = stats.information_ratio(returns, benchmark, prepare_returns=False)

np.testing.assert_allclose(result, expected)

def test_treynor_ratio(self, sample_returns, sample_benchmark):
"""Test Treynor Ratio calculation."""
result = stats.treynor_ratio(sample_returns, sample_benchmark)
Expand Down