"""Advanced metrics.
Use :term:`Quantstats` library to calculate various metrics about the strategy performance.
This will generate metrics like:
- Sharpe
- Sortino
- Max drawdown
**Note**: These metrics are based on equity curve and returns - they do go down to the individual trade level.
Any consecutive wins and losses are measured in days, not in the trade or candle count.
"""
import enum
import warnings
import pandas as pd
from tradeexecutor.state.identifier import TradingPairIdentifier
from tradeexecutor.strategy.trading_strategy_universe import TradingStrategyUniverse, translate_trading_pair
from tradeexecutor.visual.equity_curve import calculate_returns, resample_returns
from tradeexecutor.visual.qs_wrapper import import_quantstats_wrapped
from tradingstrategy.types import TokenSymbol
[docs]class AdvancedMetricsMode(enum.Enum):
"""What we will make quantstats to spit out."""
#: Less stats
basic = "basic"
#: More stats
full = "full"
[docs]def calculate_advanced_metrics(
returns: pd.Series,
mode: AdvancedMetricsMode=AdvancedMetricsMode.basic,
periods_per_year=365,
convert_to_daily=False,
benchmark: pd.Series | None = None,
display=False,
) -> pd.DataFrame:
"""Calculate advanced strategy performance statistics using Quantstats.
Calculates multiple metrics used to benchmark strategies for :term:`risk-adjusted returns`
in one go.
See :term:`Quantstats` for more information.
Example:
.. code-block:: python
from tradeexecutor.visual.equity_curve import calculate_equity_curve, calculate_returns
from tradeexecutor.analysis.advanced_metrics import calculate_advanced_metrics
equity = calculate_equity_curve(state)
returns = calculate_returns(equity)
metrics = calculate_advanced_metrics(returns)
# Each metric as a series. Index 0 is our performance,
# index 1 is the benchmark.
sharpe = metrics.loc["Sharpe"][0]
assert sharpe == pytest.approx(-1.73)
See also :py:func:`visualise_advanced_metrics`.
:param returns:
Returns series of the strategy.
See :py:`tradeeexecutor.visual.equity_curve.calculate_returns`.
:param mode:
Full or basic stats
:param periods_per_year:
How often the trade decision cycle was run.
This affects "trading periods per year" needed, to calculate
metrics like Sharpe.
The defaults to the daily trading cycle, trading 24/7.
:param convert_to_daily:
QuantStats metrics can only work on daily data, so force convert from 1h or 8h or so if needed.
:return:
DataFrame of metrics generated by quantstats.
You can directly display this in your notebook,
or extract individual metrics.
"""
# DeprecationWarning: Importing display from IPython.core.display is deprecated since IPython 7.14, please import from IPython display
with warnings.catch_warnings():
warnings.simplefilter(action='ignore', category=FutureWarning) # yfinance: The default dtype for empty Series will be 'object' instead of 'float64' in a future version. Specify a dtype explicitly to silence this warning.
warnings.simplefilter(action='ignore', category=RuntimeWarning) # Divided by Nan
qs = import_quantstats_wrapped()
metrics = qs.reports.metrics
stats = qs.stats
# QuantStats function APIs are a mess
quantstats_awful_kwargs = {}
if display:
quantstats_awful_kwargs = {"internal": True}
result = metrics(
returns,
benchmark=benchmark,
as_pct=display, # QuantStats codebase is a mess
periods_per_year=periods_per_year,
mode=mode.value,
display=False,
**quantstats_awful_kwargs
)
assert result is not None, "metrics(): returned None"
if convert_to_daily:
returns = resample_returns(returns, "D")
# Hack - see analyse_combination()
# Communicative annualized growth return,
# as compounded
# Should say CAGR (raw), but is what it is for the legacy reasons
if benchmark is None:
result.loc["Annualised return (raw)"] = [stats.cagr(returns, 0., compounded=True)]
return result
[docs]def visualise_advanced_metrics(
returns: pd.Series,
mode: AdvancedMetricsMode=AdvancedMetricsMode.basic,
benchmark: pd.Series | None = None,
name: str | None = None,
convert_to_daily=False,
) -> pd.DataFrame:
"""Calculate advanced strategy performance statistics using Quantstats.
Calculates multiple metrics used to benchmark strategies for :term:`risk-adjusted returns`
in one go.
See :term:`Quantstats` for more information.
Example:
.. code-block:: python
from tradeexecutor.visual.equity_curve import calculate_equity_curve, calculate_returns
from tradeexecutor.analysis.advanced_metrics import visualise_advanced_metrics
equity = calculate_equity_curve(state)
returns = calculate_returns(equity)
df = visualise_advanced_metrics(returns)
display(df)
Example with benchmarking against buy and hold ETH:
.. code-block:: python
from tradeexecutor.visual.equity_curve import calculate_equity_curve, calculate_returns, generate_buy_and_hold_returns
from tradeexecutor.analysis.advanced_metrics import visualise_advanced_metrics, AdvancedMetricsMode
equity = calculate_equity_curve(state)
returns = calculate_returns(equity)
benchmark_returns = generate_buy_and_hold_returns(benchmark_indexes["ETH"])
benchmark_returns.attrs["name"] = "Buy and hold ETH"
metrics = visualise_advanced_metrics(
returns,
mode=AdvancedMetricsMode.full,
benchmark=benchmark_returns,
)
display(metrics)
When dealing with 1h or 8h data:
.. code-block:: python
from tradeexecutor.analysis.advanced_metrics import visualise_advanced_metrics
visualise_advanced_metrics(
best_result.returns,
benchmark=benchmark_indexes["ETH"],
convert_to_daily=True,
)
See also :py:func:`calculate_advanced_metrics`.
:param returns:
Returns series of the strategy.
See :py:`tradeeexecutor.visual.equity_curve.calculate_returns`.
:param mode:
Full or basic stats
:param benchmark:
Benchmark portfolio or buy and hold asset.
If this series as `series.attrs["name"]` name set, it is used as a title instead of "Benchmark".
:param name:
Title oif the primary performance series instead of "Strategy".
:param convert_to_daily:
QuantStats metrics can only work on daily data, so force convert from 1h or 8h or so if needed.
:return:
A DataFrame ready to display a table of comparable merics.
Return empty DataFrame if `returns` is all zeroes.
"""
with warnings.catch_warnings():
warnings.simplefilter(action='ignore', category=FutureWarning) # yfinance: The default dtype for empty Series will be 'object' instead of 'float64' in a future version. Specify a dtype explicitly to silence this warning.
warnings.simplefilter(action='ignore', category=RuntimeWarning) # Divided by Nan
qs = import_quantstats_wrapped()
metrics = qs.reports.metrics
if not returns.any():
# Cannot calculate any metrics, because
# there has not been any trades (all returns are zero)
return pd.DataFrame()
if convert_to_daily:
returns = resample_returns(returns, "D")
if benchmark is not None:
benchmark = resample_returns(calculate_returns(benchmark), "D")
# Internal sets the flag for percent output
df = metrics(
returns,
benchmark=benchmark,
periods_per_year=365,
mode=mode.value,
internal=True,
display=False
)
# Set the label
if benchmark is not None:
benchmark_name = benchmark.attrs.get("name")
if benchmark_name:
df = df.rename({"Benchmark": benchmark_name}, axis="columns")
if name is not None:
df = df.rename({"Strategy": name}, axis="columns")
return df