pwb-toolbox 0.1.6__py3-none-any.whl → 0.1.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pwb_toolbox/backtest/__init__.py +57 -0
- pwb_toolbox/backtest/base_strategy.py +33 -0
- pwb_toolbox/backtest/execution_models/__init__.py +153 -0
- pwb_toolbox/backtest/ib_connector.py +69 -0
- pwb_toolbox/backtest/insight.py +21 -0
- pwb_toolbox/backtest/portfolio_models/__init__.py +290 -0
- pwb_toolbox/backtest/risk_models/__init__.py +175 -0
- pwb_toolbox/backtest/universe_models/__init__.py +183 -0
- pwb_toolbox/datasets/__init__.py +8 -5
- pwb_toolbox/performance/__init__.py +123 -0
- pwb_toolbox/performance/metrics.py +465 -0
- pwb_toolbox/performance/plots.py +415 -0
- pwb_toolbox/performance/trade_stats.py +138 -0
- {pwb_toolbox-0.1.6.dist-info → pwb_toolbox-0.1.8.dist-info}/METADATA +78 -3
- pwb_toolbox-0.1.8.dist-info/RECORD +19 -0
- pwb_toolbox-0.1.6.dist-info/RECORD +0 -7
- {pwb_toolbox-0.1.6.dist-info → pwb_toolbox-0.1.8.dist-info}/WHEEL +0 -0
- {pwb_toolbox-0.1.6.dist-info → pwb_toolbox-0.1.8.dist-info}/licenses/LICENSE.txt +0 -0
- {pwb_toolbox-0.1.6.dist-info → pwb_toolbox-0.1.8.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,175 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import Dict, Iterable
|
4
|
+
|
5
|
+
|
6
|
+
class RiskManagementModel:
|
7
|
+
"""Base class for risk management models."""
|
8
|
+
|
9
|
+
def evaluate(self, weights: Dict[str, float], prices: Dict[str, float]) -> Dict[str, float]:
|
10
|
+
"""Return adjusted target weights based on risk rules."""
|
11
|
+
raise NotImplementedError
|
12
|
+
|
13
|
+
|
14
|
+
class TrailingStopRiskManagementModel(RiskManagementModel):
|
15
|
+
"""Close positions if price falls a percentage from the peak."""
|
16
|
+
|
17
|
+
def __init__(self, percent: float = 0.1):
|
18
|
+
self.percent = percent
|
19
|
+
self._highs: Dict[str, float] = {}
|
20
|
+
|
21
|
+
def evaluate(self, weights: Dict[str, float], prices: Dict[str, float]) -> Dict[str, float]:
|
22
|
+
out = dict(weights)
|
23
|
+
for symbol, weight in weights.items():
|
24
|
+
price = prices.get(symbol)
|
25
|
+
if price is None:
|
26
|
+
continue
|
27
|
+
high = self._highs.get(symbol, price)
|
28
|
+
if price > high:
|
29
|
+
high = price
|
30
|
+
self._highs[symbol] = high
|
31
|
+
if weight != 0 and price <= high * (1 - self.percent):
|
32
|
+
out[symbol] = 0.0
|
33
|
+
return out
|
34
|
+
|
35
|
+
|
36
|
+
class MaximumDrawdownPercentPerSecurity(TrailingStopRiskManagementModel):
|
37
|
+
"""Alias of trailing stop for per-security drawdown."""
|
38
|
+
|
39
|
+
def __init__(self, max_drawdown: float = 0.1):
|
40
|
+
super().__init__(percent=max_drawdown)
|
41
|
+
|
42
|
+
|
43
|
+
class MaximumDrawdownPercentPortfolio(RiskManagementModel):
|
44
|
+
"""Flatten portfolio if total drawdown exceeds a threshold."""
|
45
|
+
|
46
|
+
def __init__(self, max_drawdown: float = 0.2):
|
47
|
+
self.max_drawdown = max_drawdown
|
48
|
+
self._high: float | None = None
|
49
|
+
|
50
|
+
def evaluate(self, weights: Dict[str, float], prices: Dict[str, float]) -> Dict[str, float]:
|
51
|
+
nav = sum(weights.get(s, 0.0) * prices.get(s, 0.0) for s in weights)
|
52
|
+
if self._high is None:
|
53
|
+
self._high = nav
|
54
|
+
if nav > self._high:
|
55
|
+
self._high = nav
|
56
|
+
if self._high and nav <= self._high * (1 - self.max_drawdown):
|
57
|
+
return {s: 0.0 for s in weights}
|
58
|
+
return weights
|
59
|
+
|
60
|
+
|
61
|
+
class MaximumUnrealizedProfitPercentPerSecurity(RiskManagementModel):
|
62
|
+
"""Take profit once unrealized gain exceeds threshold."""
|
63
|
+
|
64
|
+
def __init__(self, max_profit: float = 0.2):
|
65
|
+
self.max_profit = max_profit
|
66
|
+
self._entry: Dict[str, float] = {}
|
67
|
+
|
68
|
+
def evaluate(self, weights: Dict[str, float], prices: Dict[str, float]) -> Dict[str, float]:
|
69
|
+
out = dict(weights)
|
70
|
+
for symbol, weight in weights.items():
|
71
|
+
price = prices.get(symbol)
|
72
|
+
if price is None:
|
73
|
+
continue
|
74
|
+
if weight == 0:
|
75
|
+
self._entry.pop(symbol, None)
|
76
|
+
continue
|
77
|
+
entry = self._entry.get(symbol)
|
78
|
+
if entry is None:
|
79
|
+
self._entry[symbol] = price
|
80
|
+
continue
|
81
|
+
if weight > 0:
|
82
|
+
profit = (price - entry) / entry
|
83
|
+
else:
|
84
|
+
profit = (entry - price) / entry
|
85
|
+
if profit >= self.max_profit:
|
86
|
+
out[symbol] = 0.0
|
87
|
+
self._entry.pop(symbol, None)
|
88
|
+
return out
|
89
|
+
|
90
|
+
|
91
|
+
class MaximumTotalPortfolioExposure(RiskManagementModel):
|
92
|
+
"""Scale weights so total gross exposure stays below a limit."""
|
93
|
+
|
94
|
+
def __init__(self, max_exposure: float = 1.0):
|
95
|
+
self.max_exposure = max_exposure
|
96
|
+
|
97
|
+
def evaluate(self, weights: Dict[str, float], prices: Dict[str, float] | None = None) -> Dict[str, float]:
|
98
|
+
gross = sum(abs(w) for w in weights.values())
|
99
|
+
if gross <= self.max_exposure or gross == 0:
|
100
|
+
return weights
|
101
|
+
scale = self.max_exposure / gross
|
102
|
+
return {s: w * scale for s, w in weights.items()}
|
103
|
+
|
104
|
+
|
105
|
+
class SectorExposureRiskManagementModel(RiskManagementModel):
|
106
|
+
"""Limit exposure by sector."""
|
107
|
+
|
108
|
+
def __init__(self, sector_map: Dict[str, str], limit: float = 0.3):
|
109
|
+
self.sector_map = sector_map
|
110
|
+
self.limit = limit
|
111
|
+
|
112
|
+
def evaluate(self, weights: Dict[str, float], prices: Dict[str, float] | None = None) -> Dict[str, float]:
|
113
|
+
out = dict(weights)
|
114
|
+
exposures: Dict[str, float] = {}
|
115
|
+
for symbol, weight in weights.items():
|
116
|
+
sector = self.sector_map.get(symbol)
|
117
|
+
if sector is None:
|
118
|
+
continue
|
119
|
+
exposures[sector] = exposures.get(sector, 0.0) + abs(weight)
|
120
|
+
for sector, exposure in exposures.items():
|
121
|
+
if exposure > self.limit and exposure != 0:
|
122
|
+
factor = self.limit / exposure
|
123
|
+
for symbol, weight in weights.items():
|
124
|
+
if self.sector_map.get(symbol) == sector:
|
125
|
+
out[symbol] = weight * factor
|
126
|
+
return out
|
127
|
+
|
128
|
+
|
129
|
+
class MaximumOrderQuantityPercentPerSecurity(RiskManagementModel):
|
130
|
+
"""Cap the change in weight for each security per evaluation call."""
|
131
|
+
|
132
|
+
def __init__(self, max_percent: float = 0.1):
|
133
|
+
self.max_percent = max_percent
|
134
|
+
self._prev: Dict[str, float] = {}
|
135
|
+
|
136
|
+
def evaluate(self, weights: Dict[str, float], prices: Dict[str, float] | None = None) -> Dict[str, float]:
|
137
|
+
out = {}
|
138
|
+
for symbol, target in weights.items():
|
139
|
+
prev = self._prev.get(symbol, 0.0)
|
140
|
+
diff = target - prev
|
141
|
+
if diff > self.max_percent:
|
142
|
+
new = prev + self.max_percent
|
143
|
+
elif diff < -self.max_percent:
|
144
|
+
new = prev - self.max_percent
|
145
|
+
else:
|
146
|
+
new = target
|
147
|
+
out[symbol] = new
|
148
|
+
self._prev[symbol] = new
|
149
|
+
return out
|
150
|
+
|
151
|
+
|
152
|
+
class CompositeRiskManagementModel(RiskManagementModel):
|
153
|
+
"""Combine multiple risk models sequentially."""
|
154
|
+
|
155
|
+
def __init__(self, models: Iterable[RiskManagementModel]):
|
156
|
+
self.models = list(models)
|
157
|
+
|
158
|
+
def evaluate(self, weights: Dict[str, float], prices: Dict[str, float]) -> Dict[str, float]:
|
159
|
+
out = dict(weights)
|
160
|
+
for model in self.models:
|
161
|
+
out = model.evaluate(out, prices)
|
162
|
+
return out
|
163
|
+
|
164
|
+
|
165
|
+
__all__ = [
|
166
|
+
"RiskManagementModel",
|
167
|
+
"TrailingStopRiskManagementModel",
|
168
|
+
"MaximumDrawdownPercentPerSecurity",
|
169
|
+
"MaximumDrawdownPercentPortfolio",
|
170
|
+
"MaximumUnrealizedProfitPercentPerSecurity",
|
171
|
+
"MaximumTotalPortfolioExposure",
|
172
|
+
"SectorExposureRiskManagementModel",
|
173
|
+
"MaximumOrderQuantityPercentPerSecurity",
|
174
|
+
"CompositeRiskManagementModel",
|
175
|
+
]
|
@@ -0,0 +1,183 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from abc import ABC, abstractmethod
|
4
|
+
from datetime import date
|
5
|
+
from typing import Callable, Dict, Iterable, List, Sequence
|
6
|
+
|
7
|
+
import pandas as pd
|
8
|
+
|
9
|
+
from ...datasets import load_dataset
|
10
|
+
|
11
|
+
|
12
|
+
class UniverseSelectionModel(ABC):
|
13
|
+
"""Base class for universe selection models."""
|
14
|
+
|
15
|
+
@abstractmethod
|
16
|
+
def symbols(self, as_of: date | str | None = None) -> List[str]:
|
17
|
+
"""Return the active list of symbols."""
|
18
|
+
raise NotImplementedError
|
19
|
+
|
20
|
+
|
21
|
+
class ManualUniverseSelectionModel(UniverseSelectionModel):
|
22
|
+
"""Universe defined by a static list of tickers."""
|
23
|
+
|
24
|
+
def __init__(self, symbols: Sequence[str]):
|
25
|
+
self._symbols = list(symbols)
|
26
|
+
|
27
|
+
def symbols(self, as_of: date | str | None = None) -> List[str]:
|
28
|
+
return list(self._symbols)
|
29
|
+
|
30
|
+
|
31
|
+
class ScheduledUniverseSelectionModel(UniverseSelectionModel):
|
32
|
+
"""Switch universe based on a schedule of dates."""
|
33
|
+
|
34
|
+
def __init__(self, schedule: Dict[date | str, Sequence[str]]):
|
35
|
+
self.schedule = {
|
36
|
+
(pd.Timestamp(k).date() if not isinstance(k, date) else k): list(v)
|
37
|
+
for k, v in schedule.items()
|
38
|
+
}
|
39
|
+
|
40
|
+
def symbols(self, as_of: date | str | None = None) -> List[str]:
|
41
|
+
if not self.schedule:
|
42
|
+
return []
|
43
|
+
dt = pd.Timestamp(as_of or date.today()).date()
|
44
|
+
valid = [d for d in self.schedule if d <= dt]
|
45
|
+
if not valid:
|
46
|
+
return []
|
47
|
+
last = max(valid)
|
48
|
+
return self.schedule[last]
|
49
|
+
|
50
|
+
|
51
|
+
class CoarseFundamentalUniverseSelectionModel(UniverseSelectionModel):
|
52
|
+
"""Universe filtered using coarse fundamental data."""
|
53
|
+
|
54
|
+
def __init__(
|
55
|
+
self,
|
56
|
+
selector: Callable[[pd.DataFrame], Iterable[str]],
|
57
|
+
dataset: str = "Stocks-Quarterly-BalanceSheet",
|
58
|
+
):
|
59
|
+
self.selector = selector
|
60
|
+
self.dataset = dataset
|
61
|
+
|
62
|
+
def symbols(self, as_of: date | str | None = None) -> List[str]:
|
63
|
+
df = load_dataset(self.dataset)
|
64
|
+
return list(self.selector(df))
|
65
|
+
|
66
|
+
|
67
|
+
class FineFundamentalUniverseSelectionModel(UniverseSelectionModel):
|
68
|
+
"""Universe filtered using fine fundamental data."""
|
69
|
+
|
70
|
+
def __init__(
|
71
|
+
self,
|
72
|
+
selector: Callable[[pd.DataFrame], Iterable[str]],
|
73
|
+
dataset: str = "Stocks-Quarterly-Earnings",
|
74
|
+
):
|
75
|
+
self.selector = selector
|
76
|
+
self.dataset = dataset
|
77
|
+
|
78
|
+
def symbols(self, as_of: date | str | None = None) -> List[str]:
|
79
|
+
df = load_dataset(self.dataset)
|
80
|
+
return list(self.selector(df))
|
81
|
+
|
82
|
+
|
83
|
+
class ETFConstituentsUniverseSelectionModel(UniverseSelectionModel):
|
84
|
+
"""Universe containing constituents of a given ETF."""
|
85
|
+
|
86
|
+
def __init__(self, etf: str):
|
87
|
+
self.etf = etf
|
88
|
+
|
89
|
+
def symbols(self, as_of: date | str | None = None) -> List[str]:
|
90
|
+
df = load_dataset("ETF-Constituents")
|
91
|
+
if "etf" in df.columns:
|
92
|
+
col = "etf"
|
93
|
+
else:
|
94
|
+
col = df.columns[0] if df.columns else "etf"
|
95
|
+
if df.empty:
|
96
|
+
return []
|
97
|
+
return list(df[df[col] == self.etf]["symbol"].unique())
|
98
|
+
|
99
|
+
|
100
|
+
class IndexConstituentsUniverseSelectionModel(UniverseSelectionModel):
|
101
|
+
"""Universe of constituents for a specified index."""
|
102
|
+
|
103
|
+
def __init__(self, index: str):
|
104
|
+
self.index = index
|
105
|
+
|
106
|
+
def symbols(self, as_of: date | str | None = None) -> List[str]:
|
107
|
+
df = load_dataset("Index-Constituents")
|
108
|
+
if df.empty:
|
109
|
+
return []
|
110
|
+
col = "index" if "index" in df.columns else df.columns[0]
|
111
|
+
return list(df[df[col] == self.index]["symbol"].unique())
|
112
|
+
|
113
|
+
|
114
|
+
class OptionUniverseSelectionModel(UniverseSelectionModel):
|
115
|
+
"""Universe consisting of options for the given underlyings."""
|
116
|
+
|
117
|
+
def __init__(self, underlying_symbols: Sequence[str]):
|
118
|
+
self.underlyings = list(underlying_symbols)
|
119
|
+
|
120
|
+
def symbols(self, as_of: date | str | None = None) -> List[str]:
|
121
|
+
return list(self.underlyings)
|
122
|
+
|
123
|
+
|
124
|
+
class ADRUniverseSelectionModel(UniverseSelectionModel):
|
125
|
+
"""Universe of American Depositary Receipts."""
|
126
|
+
|
127
|
+
def __init__(self, dataset: str = "ADR-Listings"):
|
128
|
+
self.dataset = dataset
|
129
|
+
|
130
|
+
def symbols(self, as_of: date | str | None = None) -> List[str]:
|
131
|
+
df = load_dataset(self.dataset)
|
132
|
+
if df.empty:
|
133
|
+
return []
|
134
|
+
return list(df["symbol"].unique())
|
135
|
+
|
136
|
+
|
137
|
+
class CryptoUniverseSelectionModel(UniverseSelectionModel):
|
138
|
+
"""Universe built from cryptocurrency tickers."""
|
139
|
+
|
140
|
+
def __init__(self, top_n: int | None = None):
|
141
|
+
self.top_n = top_n
|
142
|
+
|
143
|
+
def symbols(self, as_of: date | str | None = None) -> List[str]:
|
144
|
+
df = load_dataset("Cryptocurrencies-Daily-Price")
|
145
|
+
syms = list(dict.fromkeys(df["symbol"]))
|
146
|
+
if self.top_n is not None:
|
147
|
+
syms = syms[: self.top_n]
|
148
|
+
return syms
|
149
|
+
|
150
|
+
|
151
|
+
class UniverseSelectionModelChain(UniverseSelectionModel):
|
152
|
+
"""Combine multiple universe selection models."""
|
153
|
+
|
154
|
+
def __init__(self, models: Iterable[UniverseSelectionModel]):
|
155
|
+
self.models = list(models)
|
156
|
+
|
157
|
+
def symbols(self, as_of: date | str | None = None) -> List[str]:
|
158
|
+
all_syms: List[str] = []
|
159
|
+
for m in self.models:
|
160
|
+
all_syms.extend(m.symbols(as_of))
|
161
|
+
seen = set()
|
162
|
+
uniq = []
|
163
|
+
for s in all_syms:
|
164
|
+
if s not in seen:
|
165
|
+
seen.add(s)
|
166
|
+
uniq.append(s)
|
167
|
+
return uniq
|
168
|
+
|
169
|
+
|
170
|
+
__all__ = [
|
171
|
+
"UniverseSelectionModel",
|
172
|
+
"ManualUniverseSelectionModel",
|
173
|
+
"ScheduledUniverseSelectionModel",
|
174
|
+
"CoarseFundamentalUniverseSelectionModel",
|
175
|
+
"FineFundamentalUniverseSelectionModel",
|
176
|
+
"ETFConstituentsUniverseSelectionModel",
|
177
|
+
"IndexConstituentsUniverseSelectionModel",
|
178
|
+
"OptionUniverseSelectionModel",
|
179
|
+
"ADRUniverseSelectionModel",
|
180
|
+
"CryptoUniverseSelectionModel",
|
181
|
+
"UniverseSelectionModelChain",
|
182
|
+
]
|
183
|
+
|
pwb_toolbox/datasets/__init__.py
CHANGED
@@ -6,9 +6,12 @@ import re
|
|
6
6
|
import datasets as ds
|
7
7
|
import pandas as pd
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
|
10
|
+
def _get_hf_token() -> str:
|
11
|
+
token = os.getenv("HF_ACCESS_TOKEN")
|
12
|
+
if not token:
|
13
|
+
raise ValueError("HF_ACCESS_TOKEN not set")
|
14
|
+
return token
|
12
15
|
|
13
16
|
|
14
17
|
DAILY_PRICE_DATASETS = [
|
@@ -552,7 +555,7 @@ def load_dataset(
|
|
552
555
|
to_usd=True,
|
553
556
|
rate_to_price=True,
|
554
557
|
):
|
555
|
-
dataset = ds.load_dataset(f"paperswithbacktest/{path}", token=
|
558
|
+
dataset = ds.load_dataset(f"paperswithbacktest/{path}", token=_get_hf_token())
|
556
559
|
df = dataset["train"].to_pandas()
|
557
560
|
|
558
561
|
if path in DAILY_PRICE_DATASETS or path in DAILY_FINANCIAL_DATASETS:
|
@@ -877,7 +880,7 @@ def __extend_etfs(df_etfs):
|
|
877
880
|
)
|
878
881
|
|
879
882
|
|
880
|
-
ALLOWED_FIELDS = {"open", "high", "low", "close"}
|
883
|
+
ALLOWED_FIELDS = {"open", "high", "low", "close", "volume"}
|
881
884
|
|
882
885
|
|
883
886
|
def get_pricing(
|
@@ -0,0 +1,123 @@
|
|
1
|
+
from .metrics import (
|
2
|
+
total_return,
|
3
|
+
cagr,
|
4
|
+
returns_table,
|
5
|
+
rolling_cumulative_return,
|
6
|
+
annualized_volatility,
|
7
|
+
max_drawdown,
|
8
|
+
ulcer_index,
|
9
|
+
ulcer_performance_index,
|
10
|
+
parametric_var,
|
11
|
+
parametric_expected_shortfall,
|
12
|
+
tail_ratio,
|
13
|
+
sharpe_ratio,
|
14
|
+
sortino_ratio,
|
15
|
+
calmar_ratio,
|
16
|
+
omega_ratio,
|
17
|
+
information_ratio,
|
18
|
+
capm_alpha_beta,
|
19
|
+
skewness,
|
20
|
+
kurtosis,
|
21
|
+
variance_ratio,
|
22
|
+
acf,
|
23
|
+
pacf,
|
24
|
+
fama_french_3factor,
|
25
|
+
fama_french_5factor,
|
26
|
+
cumulative_excess_return,
|
27
|
+
)
|
28
|
+
|
29
|
+
from .trade_stats import (
|
30
|
+
hit_rate,
|
31
|
+
average_win_loss,
|
32
|
+
expectancy,
|
33
|
+
profit_factor,
|
34
|
+
trade_duration_distribution,
|
35
|
+
turnover,
|
36
|
+
trade_implementation_shortfall,
|
37
|
+
cumulative_implementation_shortfall,
|
38
|
+
slippage_stats,
|
39
|
+
latency_stats,
|
40
|
+
)
|
41
|
+
|
42
|
+
__all__ = [
|
43
|
+
"total_return",
|
44
|
+
"cagr",
|
45
|
+
"returns_table",
|
46
|
+
"rolling_cumulative_return",
|
47
|
+
"annualized_volatility",
|
48
|
+
"max_drawdown",
|
49
|
+
"ulcer_index",
|
50
|
+
"ulcer_performance_index",
|
51
|
+
"parametric_var",
|
52
|
+
"parametric_expected_shortfall",
|
53
|
+
"tail_ratio",
|
54
|
+
"sharpe_ratio",
|
55
|
+
"sortino_ratio",
|
56
|
+
"calmar_ratio",
|
57
|
+
"omega_ratio",
|
58
|
+
"information_ratio",
|
59
|
+
"capm_alpha_beta",
|
60
|
+
"skewness",
|
61
|
+
"kurtosis",
|
62
|
+
"variance_ratio",
|
63
|
+
"acf",
|
64
|
+
"pacf",
|
65
|
+
"fama_french_3factor",
|
66
|
+
"fama_french_5factor",
|
67
|
+
"cumulative_excess_return",
|
68
|
+
"hit_rate",
|
69
|
+
"average_win_loss",
|
70
|
+
"expectancy",
|
71
|
+
"profit_factor",
|
72
|
+
"trade_duration_distribution",
|
73
|
+
"turnover",
|
74
|
+
"trade_implementation_shortfall",
|
75
|
+
"cumulative_implementation_shortfall",
|
76
|
+
"slippage_stats",
|
77
|
+
"latency_stats",
|
78
|
+
]
|
79
|
+
|
80
|
+
try: # pragma: no cover - optional plotting deps
|
81
|
+
from .plots import (
|
82
|
+
plot_equity_curve,
|
83
|
+
plot_return_heatmap,
|
84
|
+
plot_underwater,
|
85
|
+
plot_rolling_volatility,
|
86
|
+
plot_rolling_var,
|
87
|
+
plot_rolling_sharpe,
|
88
|
+
plot_rolling_sortino,
|
89
|
+
plot_return_scatter,
|
90
|
+
plot_cumulative_excess_return,
|
91
|
+
plot_factor_exposures,
|
92
|
+
plot_trade_return_hist,
|
93
|
+
plot_return_by_holding_period,
|
94
|
+
plot_exposure_ts,
|
95
|
+
plot_cumulative_shortfall,
|
96
|
+
plot_alpha_vs_return,
|
97
|
+
plot_qq_returns,
|
98
|
+
plot_rolling_skewness,
|
99
|
+
plot_rolling_kurtosis,
|
100
|
+
)
|
101
|
+
|
102
|
+
__all__ += [
|
103
|
+
"plot_equity_curve",
|
104
|
+
"plot_return_heatmap",
|
105
|
+
"plot_underwater",
|
106
|
+
"plot_rolling_volatility",
|
107
|
+
"plot_rolling_var",
|
108
|
+
"plot_rolling_sharpe",
|
109
|
+
"plot_rolling_sortino",
|
110
|
+
"plot_return_scatter",
|
111
|
+
"plot_cumulative_excess_return",
|
112
|
+
"plot_factor_exposures",
|
113
|
+
"plot_trade_return_hist",
|
114
|
+
"plot_return_by_holding_period",
|
115
|
+
"plot_exposure_ts",
|
116
|
+
"plot_cumulative_shortfall",
|
117
|
+
"plot_alpha_vs_return",
|
118
|
+
"plot_qq_returns",
|
119
|
+
"plot_rolling_skewness",
|
120
|
+
"plot_rolling_kurtosis",
|
121
|
+
]
|
122
|
+
except Exception: # pragma: no cover - matplotlib may be missing
|
123
|
+
pass
|