@@ -97,24 +97,34 @@ 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- s = pd .Series (dtype = object )
101- s .loc ['Start' ] = index [0 ]
102- s .loc ['End' ] = index [- 1 ]
103- s .loc ['Duration' ] = s .End - s .Start
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+ ])
104109
105110 have_position = np .repeat (0 , len (index ))
106111 for t in trades_df .itertuples (index = False ):
107112 have_position [t .EntryBar :t .ExitBar + 1 ] = 1
108113
109- s .loc ['Exposure Time [%]' ] = have_position .mean () * 100 # In "n bars" time, not index time
110- s .loc ['Equity Final [$]' ] = equity [- 1 ]
111- s .loc ['Equity Peak [$]' ] = equity .max ()
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 ))
112120 if commissions :
113- s .loc ['Commissions [$]' ] = commissions
114- s .loc ['Return [%]' ] = (equity [- 1 ] - equity [0 ]) / equity [0 ] * 100
121+ stat_items .append (('Commissions [$]' , commissions ))
122+ return_pct = (equity_final - equity [0 ]) / equity [0 ] * 100
123+ stat_items .append (('Return [%]' , return_pct ))
115124 first_trading_bar = _indicator_warmup_nbars (strategy_instance )
116125 c = ohlc_data .Close .values
117- s .loc ['Buy & Hold Return [%]' ] = (c [- 1 ] - c [first_trading_bar ]) / c [first_trading_bar ] * 100 # long-only return
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
118128
119129 gmean_day_return : float = 0
120130 day_returns = np .array (np .nan )
@@ -137,22 +147,29 @@ def _round_timedelta(value, _period=_data_period(index)):
137147 # Our annualized return matches `empyrical.annual_return(day_returns)` whereas
138148 # our risk doesn't; they use the simpler approach below.
139149 annualized_return = (1 + gmean_day_return )** annual_trading_days - 1
140- s .loc ['Return (Ann.) [%]' ] = annualized_return * 100
141- s .loc ['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
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 ))
142154 # s.loc['Return (Ann.) [%]'] = gmean_day_return * annual_trading_days * 100
143155 # s.loc['Risk (Ann.) [%]'] = day_returns.std(ddof=1) * np.sqrt(annual_trading_days) * 100
144156 if is_datetime_index :
145- time_in_years = (s .loc ['Duration' ].days + s .loc ['Duration' ].seconds / 86400 ) / annual_trading_days
146- s .loc ['CAGR [%]' ] = ((s .loc ['Equity Final [$]' ] / equity [0 ])** (1 / time_in_years ) - 1 ) * 100 if time_in_years else np .nan # noqa: E501
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 ))
147160
148161 # Our Sharpe mismatches `empyrical.sharpe_ratio()` because they use arithmetic mean return
149162 # and simple standard deviation
150- s .loc ['Sharpe Ratio' ] = (s .loc ['Return (Ann.) [%]' ] - risk_free_rate * 100 ) / (s .loc ['Volatility (Ann.) [%]' ] or np .nan ) # noqa: E501
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
151166 # Our Sortino mismatches `empyrical.sortino_ratio()` because they use arithmetic mean return
152167 with np .errstate (divide = 'ignore' ):
153- s .loc ['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
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 ))
154170 max_dd = - np .nan_to_num (dd .max ())
155- s .loc ['Calmar Ratio' ] = annualized_return / (- max_dd or np .nan )
171+ calmar_ratio = annualized_return / (- max_dd or np .nan )
172+ stat_items .append (('Calmar Ratio' , calmar_ratio ))
156173 equity_log_returns = np .log (equity [1 :] / equity [:- 1 ])
157174 market_log_returns = np .log (c [1 :] / c [:- 1 ])
158175 beta = np .nan
@@ -161,29 +178,40 @@ def _round_timedelta(value, _period=_data_period(index)):
161178 cov_matrix = np .cov (equity_log_returns , market_log_returns )
162179 beta = cov_matrix [0 , 1 ] / cov_matrix [1 , 1 ]
163180 # Jensen CAPM Alpha: can be strongly positive when beta is negative and B&H Return is large
164- s .loc ['Alpha [%]' ] = s .loc ['Return [%]' ] - risk_free_rate * 100 - beta * (s .loc ['Buy & Hold Return [%]' ] - risk_free_rate * 100 ) # noqa: E501
165- s .loc ['Beta' ] = beta
166- s .loc ['Max. Drawdown [%]' ] = max_dd * 100
167- s .loc ['Avg. Drawdown [%]' ] = - dd_peaks .mean () * 100
168- s .loc ['Max. Drawdown Duration' ] = _round_timedelta (dd_dur .max ())
169- s .loc ['Avg. Drawdown Duration' ] = _round_timedelta (dd_dur .mean ())
170- s .loc ['# Trades' ] = n_trades = len (trades_df )
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 ))
171190 win_rate = np .nan if not n_trades else (pl > 0 ).mean ()
172- s . loc [ 'Win Rate [%]' ] = win_rate * 100
173- s . loc [ 'Best Trade [%]' ] = returns .max () * 100
174- s . loc [ 'Worst Trade [%]' ] = returns .min () * 100
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 ))
175194 mean_return = geometric_mean (returns )
176- s .loc ['Avg. Trade [%]' ] = mean_return * 100
177- s .loc ['Max. Trade Duration' ] = _round_timedelta (durations .max ())
178- s .loc ['Avg. Trade Duration' ] = _round_timedelta (durations .mean ())
179- s .loc ['Profit Factor' ] = returns [returns > 0 ].sum () / (abs (returns [returns < 0 ].sum ()) or np .nan ) # noqa: E501
180- s .loc ['Expectancy [%]' ] = returns .mean () * 100
181- s .loc ['SQN' ] = np .sqrt (n_trades ) * pl .mean () / (pl .std () or np .nan )
182- s .loc ['Kelly Criterion' ] = win_rate - (1 - win_rate ) / (pl [pl > 0 ].mean () / - pl [pl < 0 ].mean ())
183-
184- s .loc ['_strategy' ] = strategy_instance
185- s .loc ['_equity_curve' ] = equity_df
186- s .loc ['_trades' ] = trades_df
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 )
187215
188216 s = _Stats (s )
189217 return s
0 commit comments