def test_max_drawdown(self): expected_max_drawdown = 0.0298 actual_max_drawdown = max_drawdown(self.test_prices_tms) self.assertAlmostEqual(expected_max_drawdown, actual_max_drawdown, delta=0.00000001) expected_max_drawdown = 0.3 prices_tms = PricesSeries(data=[100, 90, 80, 85, 70, 100], index=date_range('2015-01-01', periods=6)) actual_max_drawdown = max_drawdown(prices_tms) self.assertAlmostEqual(expected_max_drawdown, actual_max_drawdown, places=10) expected_max_drawdown = 0.35 prices_tms = PricesSeries(data=[100, 90, 80, 85, 70, 100, 90, 95, 65], index=date_range('2015-01-01', periods=9)) actual_max_drawdown = max_drawdown(prices_tms) self.assertEqual(expected_max_drawdown, actual_max_drawdown)
def test_get_weights_with_upper_limits(self): portfolio = MultiFactorPortfolio(self.assets_df.cov(), self.assets_df.var(), self.assets_df.mean(), max_drawdown(self.assets_df), self.assets_df.skew(), self.parameters, upper_constraint=0.1) actual_weights = portfolio.get_weights() expected_weights_vals = np.zeros(20) expected_weights_vals[0] = 0.1000 expected_weights_vals[4] = 0.1000 expected_weights_vals[6] = 0.1000 expected_weights_vals[7] = 0.1000 expected_weights_vals[8] = 0.0234 expected_weights_vals[10] = 0.1000 expected_weights_vals[11] = 0.0920 expected_weights_vals[13] = 0.0718 expected_weights_vals[16] = 0.1000 expected_weights_vals[17] = 0.0128 expected_weights_vals[18] = 0.1000 expected_weights_vals[19] = 0.1000 self.assertTrue( np.allclose(expected_weights_vals, actual_weights.values, rtol=0, atol=5e-02))
def _calculate_risk_stats(self): self.cvar = cvar(self.returns_tms, 0.05) # default is the 5% CVaR log_cvar = simple_to_log_return(self.cvar) annualised_log_cvar = annualise_with_sqrt(log_cvar, self.frequency) self.annualised_cvar = log_to_simple_return(annualised_log_cvar) prices_tms = self.returns_tms.to_prices() self.max_drawdown = max_drawdown(prices_tms) self.avg_drawdown = avg_drawdown(prices_tms) self.avg_drawdown_duration = avg_drawdown_duration(prices_tms)
def trade_based_max_drawdown(trades: QFDataFrame): """ Calculates the max drawdown on the series of returns of trades """ if trades.shape[0] > 0: returns = trades[TradeField.Return] dates = trades[TradeField.EndDate] returns_tms = SimpleReturnsSeries(index=dates, data=returns.values) prices_tms = returns_tms.to_prices(frequency=Frequency.DAILY) return -max_drawdown(prices_tms) return None
def _add_statistics_table(self): table = Table(column_names=["Measure", "Value"], css_class="table stats-table") number_of_trades = self.returns_of_trades.count() table.add_row(["Number of trades", number_of_trades]) period_length = self.end_date - self.start_date period_length_in_years = to_days(period_length) / DAYS_PER_YEAR_AVG avg_number_of_trades = number_of_trades / period_length_in_years / self.nr_of_assets_traded table.add_row(["Avg number of trades per year per asset", avg_number_of_trades]) positive_trades = self.returns_of_trades[self.returns_of_trades > 0] negative_trades = self.returns_of_trades[self.returns_of_trades < 0] percentage_of_positive = positive_trades.count() / number_of_trades percentage_of_negative = negative_trades.count() / number_of_trades table.add_row(["% of positive trades", percentage_of_positive * 100]) table.add_row(["% of negative trades", percentage_of_negative * 100]) avg_positive = positive_trades.mean() avg_negative = negative_trades.mean() table.add_row(["Avg positive trade [%]", avg_positive * 100]) table.add_row(["Avg negative trade [%]", avg_negative * 100]) best_return = max(self.returns_of_trades) worst_return = min(self.returns_of_trades) table.add_row(["Best trade [%]", best_return * 100]) table.add_row(["Worst trade [%]", worst_return * 100]) max_dd = max_drawdown(self.returns_of_trades) table.add_row(["Max drawdown [%]", max_dd * 100]) prices_tms = self.returns_of_trades.to_prices() total_return = prices_tms.iloc[-1] / prices_tms.iloc[0] - 1 table.add_row(["Total return [%]", total_return * 100]) annualised_ret = annualise_total_return(total_return, period_length_in_years, SimpleReturnsSeries) table.add_row(["Annualised return [%]", annualised_ret * 100]) avg_return = self.returns_of_trades.mean() table.add_row(["Avg return of trade [%]", avg_return * 100]) std_of_returns = self.returns_of_trades.std() table.add_row(["Std of return of trades [%]", std_of_returns * 100]) # System Quality Number sqn = avg_return / std_of_returns table.add_row(["SQN", sqn]) table.add_row(["SQN for 100 trades", sqn * 10]) # SQN * sqrt(100) table.add_row(["SQN * Sqrt(avg number of trades per year)", sqn * sqrt(avg_number_of_trades)]) self.document.add_element(table)
def make_stats(self, initial_risks: Sequence[float], scenarios_list: Sequence[QFDataFrame]) -> QFDataFrame: """ Creates a pandas.DataFrame showing how many strategies failed (reached certain draw down level) and how many of them succeeded (that is: reached the target return and not failed on the way). Parameters ---------- initial_risks: Sequence[float] list of initial_risk parameters where initial_risk is a float number scenarios_list: Sequence[pandas.DataFrame] list with scenarios (QFDataFrame) where each DataFrame corresponds to one initial_risk value Each DataFrame has columns corresponding to different scenarios and its indexed by Trades' ordinal number. Its values are returns of Trades. Returns ------- pandas.DataFrame DataFrame indexed with initial_risk values and with columns FAILED (fraction of scenarios that failed) and SUCCEEDED (fraction of scenarios that met the objective and didn't fail on the way) """ result = QFDataFrame(index=pd.Index(initial_risks), columns=pd.Index([self.FAILED, self.SUCCEEDED]), dtype=np.float64) for init_risk, scenarios in zip(initial_risks, scenarios_list): # calculate drawdown for each scenario scenarios_df = cast_dataframe( scenarios, SimpleReturnsDataFrame) # type: SimpleReturnsDataFrame max_drawdowns = max_drawdown(scenarios_df) total_returns = scenarios_df.total_cumulative_return() failed = max_drawdowns >= self._max_accepted_dd reached_target_return = total_returns >= self._target_return succeeded = ~failed & reached_target_return num_of_scenarios = scenarios_df.num_of_columns failed_normalized = failed.sum() / num_of_scenarios succeeded_normalized = succeeded.sum() / num_of_scenarios result.loc[init_risk, [self.FAILED, self.SUCCEEDED]] = [ failed_normalized, succeeded_normalized ] return result
def calmar_ratio(qf_series: QFSeries, frequency: Frequency) -> float: """ Calculates the Calmar ratio for a given timeseries of returns. calmar_ratio = CAGR / max drawdown Parameters ---------- qf_series financial series frequency frequency of qf_series Returns ------- calmar_ratio """ annualised_growth_rate = cagr(qf_series, frequency) max_dd = max_drawdown(qf_series) ratio = annualised_growth_rate / max_dd return ratio
def test_get_weights(self): portfolio = MultiFactorPortfolio(self.assets_df.cov(), self.assets_df.var(), self.assets_df.mean(), max_drawdown(self.assets_df), self.assets_df.skew(), self.parameters) actual_weights = portfolio.get_weights() expected_weights_vals = np.zeros(20) expected_weights_vals[4] = 0.2802 expected_weights_vals[6] = 0.0393 expected_weights_vals[16] = 0.0537 expected_weights_vals[18] = 0.4746 expected_weights_vals[19] = 0.1521 self.assertTrue( np.allclose(expected_weights_vals, actual_weights.values, rtol=0, atol=5e-02))
def _get_monte_carlos_simulator_outputs(self, scenarios_df: PricesDataFrame, total_returns: SimpleReturnsSeries) \ -> DFTable: _, all_scenarios_number = scenarios_df.shape rows = [] # Add the Median Return value median_return = np.median(total_returns) rows.append(("Median Return", "{:.2%}".format(median_return))) # Add the Mean Return value mean_return = total_returns.mean() rows.append(("Mean Return", "{:.2%}".format(mean_return))) trade_returns = QFSeries(data=[trade.percentage_pnl for trade in self.trades]) sample_len = int(self._average_number_of_trades_per_year()) std = trade_returns.std() expectation_adj_series = np.ones(sample_len) * (trade_returns.mean() - 0.5 * std * std) expectation_adj_series = SimpleReturnsSeries(data=expectation_adj_series) expectation_adj_series = expectation_adj_series.to_prices(suggested_initial_date=0) mean_volatility_adjusted_return = expectation_adj_series.iloc[-1] / expectation_adj_series.iloc[0] - 1.0 rows.append(("Mean Volatility Adjusted Return", "{:.2%}".format(mean_volatility_adjusted_return))) # Add the Median Drawdown max_drawdowns = max_drawdown(scenarios_df) median_drawdown = np.median(max_drawdowns) rows.append(("Median Maximum Drawdown", "{:.2%}".format(median_drawdown))) # Add the Median Return / Median Drawdown rows.append(("Return / Drawdown", "{:.2f}".format(median_return / median_drawdown))) # Probability, that the return will be > 0 scenarios_with_positive_result = total_returns[total_returns > 0.0].count() probability = scenarios_with_positive_result / all_scenarios_number rows.append(("Probability of positive return", "{:.2%}".format(probability))) table = DFTable(data=QFDataFrame.from_records(rows, columns=["Measure", "Value"]), css_classes=['table', 'left-align']) table.add_columns_classes(["Measure"], 'wide-column') return table