@@ -97,34 +97,24 @@ def _round_timedelta(value, _period=_data_period(index)):
9797 resolution = getattr (_period , 'resolution_string' , None ) or _period .resolution
9898 return value .ceil (resolution )
9999
100- stat_items : list [tuple [str , object ]] = []
101- start = index [0 ]
102- end = index [- 1 ]
103- duration = end - start
104- stat_items .extend ([
105- ('Start' , start ),
106- ('End' , end ),
107- ('Duration' , duration ),
108- ])
100+ s = dict ()
101+ s ['Start' ] = index [0 ]
102+ s ['End' ] = index [- 1 ]
103+ s ['Duration' ] = s ['End' ] - s ['Start' ]
109104
110105 have_position = np .repeat (0 , len (index ))
111106 for t in trades_df [['EntryBar' , 'ExitBar' ]].itertuples (index = False ):
112107 have_position [t .EntryBar :t .ExitBar + 1 ] = 1
113108
114- exposure_time_pct = have_position .mean () * 100 # In "n bars" time, not index time
115- stat_items .append (('Exposure Time [%]' , exposure_time_pct ))
116- equity_final = equity [- 1 ]
117- equity_peak = equity .max ()
118- stat_items .append (('Equity Final [$]' , equity_final ))
119- stat_items .append (('Equity Peak [$]' , equity_peak ))
109+ s ['Exposure Time [%]' ] = have_position .mean () * 100 # In "n bars" time, not index time
110+ s ['Equity Final [$]' ] = equity [- 1 ]
111+ s ['Equity Peak [$]' ] = equity .max ()
120112 if commissions :
121- stat_items .append (('Commissions [$]' , commissions ))
122- return_pct = (equity_final - equity [0 ]) / equity [0 ] * 100
123- stat_items .append (('Return [%]' , return_pct ))
113+ s ['Commissions [$]' ] = commissions
114+ s ['Return [%]' ] = (equity [- 1 ] - equity [0 ]) / equity [0 ] * 100
124115 first_trading_bar = _indicator_warmup_nbars (strategy_instance )
125116 c = ohlc_data .Close .values
126- buy_hold_return_pct = (c [- 1 ] - c [first_trading_bar ]) / c [first_trading_bar ] * 100
127- stat_items .append (('Buy & Hold Return [%]' , buy_hold_return_pct )) # long-only return
117+ s ['Buy & Hold Return [%]' ] = (c [- 1 ] - c [first_trading_bar ]) / c [first_trading_bar ] * 100 # long-only return
128118
129119 gmean_day_return : float = 0
130120 day_returns = np .array (np .nan )
@@ -147,29 +137,22 @@ def _round_timedelta(value, _period=_data_period(index)):
147137 # Our annualized return matches `empyrical.annual_return(day_returns)` whereas
148138 # our risk doesn't; they use the simpler approach below.
149139 annualized_return = (1 + gmean_day_return )** annual_trading_days - 1
150- return_ann_pct = annualized_return * 100
151- volatility_ann_pct = np .sqrt ((day_returns .var (ddof = int (bool (day_returns .shape ))) + (1 + gmean_day_return )** 2 )** annual_trading_days - (1 + gmean_day_return )** (2 * annual_trading_days )) * 100 # noqa: E501
152- stat_items .append (('Return (Ann.) [%]' , return_ann_pct ))
153- stat_items .append (('Volatility (Ann.) [%]' , volatility_ann_pct ))
154- # s.loc['Return (Ann.) [%]'] = gmean_day_return * annual_trading_days * 100
155- # s.loc['Risk (Ann.) [%]'] = day_returns.std(ddof=1) * np.sqrt(annual_trading_days) * 100
140+ s ['Return (Ann.) [%]' ] = annualized_return * 100
141+ s ['Volatility (Ann.) [%]' ] = np .sqrt ((day_returns .var (ddof = int (bool (day_returns .shape ))) + (1 + gmean_day_return )** 2 )** annual_trading_days - (1 + gmean_day_return )** (2 * annual_trading_days )) * 100 # noqa: E501
142+ # d['Return (Ann.) [%]'] = gmean_day_return * annual_trading_days * 100
143+ # d['Risk (Ann.) [%]'] = day_returns.std(ddof=1) * np.sqrt(annual_trading_days) * 100
156144 if is_datetime_index :
157- time_in_years = (duration .days + duration .seconds / 86400 ) / annual_trading_days
158- cagr_pct = ((equity_final / equity [0 ])** (1 / time_in_years ) - 1 ) * 100 if time_in_years else np .nan # noqa: E501
159- stat_items .append (('CAGR [%]' , cagr_pct ))
145+ time_in_years = (s ['Duration' ].days + s ['Duration' ].seconds / 86400 ) / annual_trading_days
146+ s ['CAGR [%]' ] = ((s ['Equity Final [$]' ] / equity [0 ])** (1 / time_in_years ) - 1 ) * 100 if time_in_years else np .nan # noqa: E501
160147
161148 # Our Sharpe mismatches `empyrical.sharpe_ratio()` because they use arithmetic mean return
162149 # and simple standard deviation
163- sharpe_denom = volatility_ann_pct or np .nan
164- sharpe_ratio = (return_ann_pct - risk_free_rate * 100 ) / sharpe_denom
165- stat_items .append (('Sharpe Ratio' , sharpe_ratio )) # noqa: E501
150+ s ['Sharpe Ratio' ] = (s ['Return (Ann.) [%]' ] - risk_free_rate * 100 ) / (s ['Volatility (Ann.) [%]' ] or np .nan ) # noqa: E501
166151 # Our Sortino mismatches `empyrical.sortino_ratio()` because they use arithmetic mean return
167152 with np .errstate (divide = 'ignore' ):
168- sortino_ratio = (annualized_return - risk_free_rate ) / (np .sqrt (np .mean (day_returns .clip (- np .inf , 0 )** 2 )) * np .sqrt (annual_trading_days )) # noqa: E501
169- stat_items .append (('Sortino Ratio' , sortino_ratio ))
153+ s ['Sortino Ratio' ] = (annualized_return - risk_free_rate ) / (np .sqrt (np .mean (day_returns .clip (- np .inf , 0 )** 2 )) * np .sqrt (annual_trading_days )) # noqa: E501
170154 max_dd = - np .nan_to_num (dd .max ())
171- calmar_ratio = annualized_return / (- max_dd or np .nan )
172- stat_items .append (('Calmar Ratio' , calmar_ratio ))
155+ s ['Calmar Ratio' ] = annualized_return / (- max_dd or np .nan )
173156 equity_log_returns = np .log (equity [1 :] / equity [:- 1 ])
174157 market_log_returns = np .log (c [1 :] / c [:- 1 ])
175158 beta = np .nan
@@ -178,42 +161,31 @@ def _round_timedelta(value, _period=_data_period(index)):
178161 cov_matrix = np .cov (equity_log_returns , market_log_returns )
179162 beta = cov_matrix [0 , 1 ] / cov_matrix [1 , 1 ]
180163 # Jensen CAPM Alpha: can be strongly positive when beta is negative and B&H Return is large
181- alpha_pct = return_pct - risk_free_rate * 100 - beta * (buy_hold_return_pct - risk_free_rate * 100 ) # noqa: E501
182- stat_items .append (('Alpha [%]' , alpha_pct ))
183- stat_items .append (('Beta' , beta ))
184- stat_items .append (('Max. Drawdown [%]' , max_dd * 100 ))
185- stat_items .append (('Avg. Drawdown [%]' , - dd_peaks .mean () * 100 ))
186- stat_items .append (('Max. Drawdown Duration' , _round_timedelta (dd_dur .max ())))
187- stat_items .append (('Avg. Drawdown Duration' , _round_timedelta (dd_dur .mean ())))
188- n_trades = len (trades_df )
189- stat_items .append (('# Trades' , n_trades ))
164+ s ['Alpha [%]' ] = s ['Return [%]' ] - risk_free_rate * 100 - beta * (s ['Buy & Hold Return [%]' ] - risk_free_rate * 100 ) # noqa: E501
165+ s ['Beta' ] = beta
166+ s ['Max. Drawdown [%]' ] = max_dd * 100
167+ s ['Avg. Drawdown [%]' ] = - dd_peaks .mean () * 100
168+ s ['Max. Drawdown Duration' ] = _round_timedelta (dd_dur .max ())
169+ s ['Avg. Drawdown Duration' ] = _round_timedelta (dd_dur .mean ())
170+ s ['# Trades' ] = n_trades = len (trades_df )
190171 win_rate = np .nan if not n_trades else (pl > 0 ).mean ()
191- stat_items . append (( 'Win Rate [%]' , win_rate * 100 ))
192- stat_items . append (( 'Best Trade [%]' , returns .max () * 100 ))
193- stat_items . append (( 'Worst Trade [%]' , returns .min () * 100 ))
172+ s [ 'Win Rate [%]' ] = win_rate * 100
173+ s [ 'Best Trade [%]' ] = returns .max () * 100
174+ s [ 'Worst Trade [%]' ] = returns .min () * 100
194175 mean_return = geometric_mean (returns )
195- stat_items .append (('Avg. Trade [%]' , mean_return * 100 ))
196- stat_items .append (('Max. Trade Duration' , _round_timedelta (durations .max ())))
197- stat_items .append (('Avg. Trade Duration' , _round_timedelta (durations .mean ())))
198- profit_factor = returns [returns > 0 ].sum () / (abs (returns [returns < 0 ].sum ()) or np .nan ) # noqa: E501
199- stat_items .append (('Profit Factor' , profit_factor ))
200- expectancy = returns .mean () * 100
201- stat_items .append (('Expectancy [%]' , expectancy ))
202- sqn = np .sqrt (n_trades ) * pl .mean () / (pl .std () or np .nan )
203- stat_items .append (('SQN' , sqn ))
204- kelly = win_rate - (1 - win_rate ) / (pl [pl > 0 ].mean () / - pl [pl < 0 ].mean ())
205- stat_items .append (('Kelly Criterion' , kelly ))
206-
207- stat_items .extend ([
208- ('_strategy' , strategy_instance ),
209- ('_equity_curve' , equity_df ),
210- ('_trades' , trades_df ),
211- ])
212-
213- labels , values = zip (* stat_items )
214- s = pd .Series (values , index = labels , dtype = object )
215-
216- s = _Stats (s )
176+ s ['Avg. Trade [%]' ] = mean_return * 100
177+ s ['Max. Trade Duration' ] = _round_timedelta (durations .max ())
178+ s ['Avg. Trade Duration' ] = _round_timedelta (durations .mean ())
179+ s ['Profit Factor' ] = returns [returns > 0 ].sum () / (abs (returns [returns < 0 ].sum ()) or np .nan ) # noqa: E501
180+ s ['Expectancy [%]' ] = returns .mean () * 100
181+ s ['SQN' ] = np .sqrt (n_trades ) * pl .mean () / (pl .std () or np .nan )
182+ s ['Kelly Criterion' ] = win_rate - (1 - win_rate ) / (pl [pl > 0 ].mean () / - pl [pl < 0 ].mean ())
183+
184+ s ['_strategy' ] = strategy_instance
185+ s ['_equity_curve' ] = equity_df
186+ s ['_trades' ] = trades_df
187+
188+ s = _Stats (s , dtype = object )
217189 return s
218190
219191
0 commit comments