Source code for simple_back.metrics

from abc import ABC, abstractmethod
from typing import List, Optional
import numpy as np
import pandas as pd
import math

from .exceptions import MissingMetricsError


[docs]class Metric(ABC): @property def requires(self) -> Optional[List[type]]: return None def __str__(self): return self.__repr__() def __repr__(self): if (self._single and self.value is None) or ( self._series and self.values is None ): return "None" if self._single: return f"{self.value:.2f}" if self._series: return f"{self[-1]:.2f}" else: return f"{self.name}" def __init__(self): self._single = False self._series = False self.value = None self.values = None self.current_event = "open" self.bt = None @property @abstractmethod def name(self) -> str: pass
[docs] def set_values(self, bt): if self._single: self.value = self.get_value(bt) if self._series: self.all_values[self.i] = self.get_value(bt)
def __call__(self, write=False): if not write: return self.get_value(self.bt) self.current_event = self.bt.event self.i = self.bt.i if self._series and (self.bt.i == 0 and self.bt.event == "open"): self.all_values = np.repeat(np.nan, len(self.bt)) if self.requires is None: self.set_values(self.bt) else: all_requires_present = True missing = "" for req in self.requires: if req not in self.bt.metric.keys(): all_requires_present = False missing = req break if all_requires_present: self.set_values(self.bt) else: raise MissingMetricsError( self.requires, f""" The following metric required by {type(self)} is missing: {missing} """, )
[docs] @abstractmethod def get_value(self, bt): pass
[docs]class SingleMetric(Metric): def __init__(self, name: Optional[str] = None): self._single = True self._series = False self.value = None @property @abstractmethod def name(self): pass
[docs] @abstractmethod def get_value(self, bt): pass
[docs]class SeriesMetric(Metric): def __init__(self, name: Optional[str] = None): self._single = False self._series = True self.value_open = [] self.value_close = [] self.i = 0 self.last_len = 0 self.all_values = [] @property def values(self): return self.all_values @property def df(self): df = pd.DataFrame() df["date"] = self.bt.dates[: self.i // 2 + 1] df["open"] = self.all_values[0::2][: self.i // 2 + 1] df["close"] = self.all_values[1::2][: self.i // 2 + 1] if self.current_event == "open": df.at[-1, "close"] = None return df.set_index("date").dropna(how="all") @property @abstractmethod def name(self): pass def __len__(self): return self.i + 1 def __getitem__(self, i): return self.all_values[: self.i + 1][i]
[docs] @abstractmethod def get_value(self, bt): pass
[docs]class MaxDrawdown(SingleMetric): @property def name(self): return "Max Drawdown (%)"
[docs] def get_value(self, bt): highest_peaks = bt.metrics["Total Value"].cummax() actual_value = bt.metrics["Total Value"] md = np.min(((actual_value - highest_peaks) / highest_peaks).values) * 100 return md
[docs]class AnnualReturn(SingleMetric): @property def name(self): return "Annual Return" @property def requires(self): return ["Portfolio Value"]
[docs] def get_value(self, bt): vals = bt.metric["Total Value"] year = 1 / ((bt.dates[-1] - bt.dates[0]).days / 365.25) return (vals[-1] / vals[0]) ** year
[docs]class PortfolioValue(SeriesMetric): @property def name(self): return "Portfolio Value"
[docs] def get_value(self, bt): return bt.portfolio.total_value
[docs]class DailyProfitLoss(SeriesMetric): @property def name(self): return "Daily Profit/Loss" @property def requires(self): return ["Total Value"]
[docs] def get_value(self, bt): try: return bt.metric["Total Value"][-1] - bt.metric["Total Value"][-3] except IndexError: return 0
[docs]class TotalValue(SeriesMetric): @property def name(self): return "Total Value" @property def requires(self): return ["Portfolio Value"]
[docs] def get_value(self, bt): return bt.metric["Portfolio Value"]() + bt._available_capital
[docs]class TotalReturn(SeriesMetric): @property def name(self): return "Total Return (%)" @property def requires(self): return ["Total Value"]
[docs] def get_value(self, bt): return ((bt.metric["Total Value"][-1] / bt.balance.start) - 1) * 100
[docs]class SharpeRatio(SingleMetric): def __init__(self, risk_free_rate=0.0445): self.risk_free_rate = 0.0445 super().__init__() @property def requires(self): return ["Volatility (Annualized)", "Annual Return"] @property def name(self): return "Sharpe Ratio"
[docs] def get_value(self, bt): return ( (bt.metrics["Annual Return"][-1] - 1) - self.risk_free_rate ) / bt.metrics["Volatility (Annualized)"][-1]
[docs]class Volatility(SingleMetric): @property def requires(self): return ["Daily Profit/Loss (%)"] @property def name(self): return "Volatility (Annualized)"
[docs] def get_value(self, bt): return bt.metric["Daily Profit/Loss (%)"].values.std() * math.sqrt(252)
[docs]class DailyProfitLossPct(SeriesMetric): @property def name(self): return "Daily Profit/Loss (%)" @property def requires(self): return ["Daily Profit/Loss", "Total Value"]
[docs] def get_value(self, bt): try: return bt.metric["Daily Profit/Loss"][-1] / bt.metric["Total Value"][-1] except IndexError: return 0