The risk-reward ratio measures the potential profit for every dollar risked. It is the ratio between the value at risk and the profit target. For example, if you buy a stock for $10 with a profit target of $12 and set a stop-loss at $9, the risk-reward ratio is 1:2 because you’re risking $1 to make $2.

In this post, I’m going to elaborate on the risk-reward ratio definition above, why it’s only half the equation, and backtest how various risk-reward ratios perform for an equity momentum strategy. There are quite a few trading books that state trading is easy: If you are disciplined and always take profits larger than your losses, you can lose more often than you win and still make money. Let’s put this to the test.

Also, credit to Aswath Damodaran for introducing me to a great representation of risk: the chinese symbol for crisis and opportunity.

The Risk-Reward Ratio Further Explained

The risk-reward ratio, often abbreviated as RRR, is the number of dollars at risk compared to the potential profit. Most traders target a RRR, such as 1:2, ahead of placing a trade. Placing a targetted “RRR” trade will take three orders or one bracket order. Let’s review these orders in detail.

The number of dollars at risk, or the value at risk, is the total amount of money that a trader can lose on a trade determined by the stop-loss, not including slippage:

Risk = (Purchase Price - Stop-Loss) * Shares

The potential profit, or the profit target, is the difference between the sell order, usually placed as a limit order, and the purchase price:

Potential Profit = (Limit Order - Purchase Price) * Shares

See the example image below for a screenshot of AMD from TradingView demonstrating a risk(red):reward(green) ratio of 2.0.

AMD

Risk-Reward Ratio Orders in Detail

We would need to place three orders with our broker in order to target a RRR when “going long”:

  1. An entry order generally set as a limit or stop-limit
  2. A risk-limiting low-side order, typically placed as a stop-loss
  3. A profit-taking high-side order, usually set as a limit order to take profit

We would place the opposite when “going short”:

  1. An entry order generally set as a limit or stop-limit
  2. A risk-limiting high-side order, typically placed as a stop-loss
  3. A profit-taking low-side order, usually set as a limit order to take profit

Some brokers support bracket orders, which simplifies the management of risk-reward targetting. Instead of placing three orders, you can create one bracket order.

So what should your risk-reward ratio be? Without knowing your winning percentage, you can’t establish an optimal RRR:

Risk-Reward Ratio: Only Half The Picture

A risk-reward ratio of 1:2 may sound great in theory, but if you’re only successful with 1:4 of your trades, you’re going to be losing money consistently. That’s not what we want!

We have to bring in the average win percentage for the trade. Let’s say we have a 1:2 RRR and a 50% win percentage. Again a 1:2 RRR means we’re risking $1.00 to make $2.00. If our trades are successful 50% of the time, we can figure out what we expect to make on each trade over the long run:

Expectancy = AverageWin * WinPercent - AverageLoss * LossPercent
Expectancy = 2.0 * 50% - 1.0 * 50%
Expectancy = 100% - 50%
Expectancy = 50%

This expectancy would indicate that we should expect $0.50 for every $1.00 we risk. The math makes logical sense, too. On average, we’ll have one winning trade and one losing trade. If we earn $2.00 on one trade and lose $1.00 on the next trade, we’ll make $1.00 over two trades or $0.50 per trade on average. Remember, we’re only making $2.00 and not $3.00 as $3.00 is our target, but $3.00 - $1.00 is our profit.

Obviously, time is a crucial factor here, too. If there are two trade setups with the same expectency, but one trade occurs twice as frequently, the higher frequency trade will make double the profit.

The Optimal Risk-Reward Ratio

Let’s see how our equity momentum strategy performs at various risk-reward ratios.

Our strategy appears to beat the SPY. See the images below for a 10% stop and 2 ATR stop both using a 1:2 risk-reward ratio. I’ve ordered the results first by P&L and then by win percentage.

Risk-Reward

RRRWin%StopPnLDrawdown
40.58%25%$44,411.8027.41%
30.58%25%$43,979.3927.40%
20.58%20%$43,111.4925.52%
30.58%20%$43,098.2926.11%
40.58%20%$42,886.2826.16%
20.58%25%$42,799.5527.54%
10.59%25%$42,697.6227.72%
20.58%15%$41,317.2028.68%
10.58%20%$40,882.0626.02%
10.59%15%$39,924.1327.72%
40.58%15%$38,878.5128.17%
30.58%15%$38,766.1328.49%
40.56%10%$37,107.7626.50%
30.56%10%$36,983.2926.95%
20.56%10%$36,174.6225.71%
10.58%10%$29,568.0025.95%
40.47%5%$28,370.3021.49%
30.47%5%$26,661.1822.11%
20.49%5%$23,676.8719.53%
10.56%5%$17,756.5620.65%
RRRWin%StopPnLDrawdown
159%15%$39,924.1327.72%
159%25%$42,697.6227.72%
158%20%$40,882.0626.02%
258%25%$42,799.5527.54%
358%25%$43,979.3927.40%
458%25%$44,411.827.41%
358%20%$43,098.2926.11%
158%10%$29,568.025.95%
258%20%$43,111.4925.52%
458%20%$42,886.2826.16%
258%15%$41,317.2028.68%
458%15%$38,878.5128.17%
358%15%$38,766.1328.49%
156%5%$17,756.5620.65%
256%10%$36,174.6225.71%
356%10%$36,983.2926.95%
456%10%$37,107.7626.50%
249%5%$23,676.8719.53%
347%5%$26,661.1822.11%
447%5%$28,370.3021.49%

ATR

RRRWin%ATRP&LMax Drawdown
10.59%5$41,386.9627.10%
20.57%5$41,157.9927.75%
30.57%5$40,034.0728.22%
10.58%4$38,716.6826.62%
30.56%4$38,639.2327.63%
40.56%4$38,422.2227.85%
20.57%4$37,881.3426.51%
40.57%5$37,637.3127.92%
40.53%3$35,955.1326.12%
30.53%3$35,838.2225.18%
20.54%3$35,420.9524.89%
40.48%2$30,487.5622.30%
10.57%3$29,660.2722.45%
30.48%2$29,253.4622.47%
20.49%2$28,330.3921.87%
10.56%2$27,552.520.18%
40.36%1$21,250.917.18%
30.38%1$19,331.2117.27%
20.42%1$16,188.8917.34%
10.55%1$10,905.3416.06%
RRRWin%ATRP&LMax Drawdown
159%5$41,386.9627.10%
158%4$38,716.6826.62%
257%5$41,157.9927.75%
357%5$40,034.0728.22%
457%5$37,637.3127.92%
157%3$29,660.2722.45%
257%4$37,881.3426.51%
156%2$27,552.520.18%
356%4$38,639.2327.63%
456%4$38,422.2227.85%
155%1$10,905.3416.06%
254%3$35,420.9524.89%
353%3$35,838.2225.18%
453%3$35,955.1326.12%
249%2$28,330.3921.87%
348%2$29,253.4622.47%
448%2$30,487.5622.30%
242%1$16,188.8917.34%
338%1$19,331.2117.27%
436%1$21,250.9017.18%

Risk-Reward Ratio Summarized

While our strategy makes money and beats the benchmark, it’s not due to any stop-loss or risk-reward ratio magic: It’s due to momentum. This is easily seen below by setting our stop-loss to 99%, which is a proxy for no stop-loss or RRR profit taking:

RRRWin%StopP&LMax Drawdown
20.58%99%$45,448.7224.60%

So the next time someone tells you trading is easy and all you need is excellent money management, dig in. Take what they’re saying and test it over multiple timeframes, instruments, strategies, and markets. I’m not saying RRR targetting and bracket orders don’t work, but I am saying test everything! Also, investing and trading may be the most competitive game in town – in other words, it’s not easy.

The Code

As always, the code will be on the Analyzing Alpha Github

The code is almost identical to the optimal stop loss for stocks posted previously. The only difference is we rebalanced weekly instead of monthly, I’ve added a profit-taking order, and added trade win percentage. I chose to show each of the individual orders, but we could have used a single bracket order instead.

from datetime import datetime, timedelta import math import backtrader as bt from positions.securities import get_security_data, get_securities_data,/ get_sp500_tickers from indicators.momentum import momentum START_DATE = 2010-01-01 END_DATE = 2019-12-31 START = datetime.strptime(START_DATE, %Y-%m-%d) END = datetime.strptime(END_DATE, %Y-%m-%d) BENCHMARK_TICKER = SPY EXCLUDE_WINDOW = 10 MOMENTUM_WINDOW = 90 MINIMUM_PERIOD = MOMENTUM_WINDOW + EXCLUDE_WINDOW POSITIONS = 20 USE_ATR = False class Momentum(bt.ind.OperationN): lines = (trend,) params = dict(period=MINIMUM_PERIOD, exclude_window=EXCLUDE_WINDOW) func = momentum def __init__(self): self.addminperiod(self.p.period) self.exclude_window = self.p.exclude_window class Strategy(bt.Strategy): params = dict( num_positions=POSITIONS, use_atr=USE_ATR, rrr=2.0, stop_loss=0.05, atr_factor=3.0, when=bt.timer.SESSION_START, timer=True, weekdays=[1], weekcarry=True, momentum=Momentum, momentum_period=MINIMUM_PERIOD ) def __init__(self): self.d_with_len = [] self.orders = {} self.inds = {} self.rebalance_date = None self.add_timer( when=self.p.when, weekdays=self.p.weekdays, weekcarry=self.p.weekcarry ) for d in self.datas[1:]: self.orders[d] = [] self.inds[d] = {} self.inds[d][momentum] = self.p.momentum(d, period=MINIMUM_PERIOD, plot=False) self.inds[d][atr] = bt.indicators.ATR(d, period=14) def prenext(self): # Add data for datas that meet preprocessing requirements  # And call next even though data is not available for all tickers  self.d_with_len = [d for d in self.datas[1:] if len(d)] if len(self.d_with_len) >= self.p.num_positions: self.next() def nextstart(self): # This is only called once when all data is present  # So we are not unnecessarily calculating d_with_len  self.d_with_len = self.datas[1:] self.next() print(All datas loaded) def next(self): if self.rebalance_date: today = self.data.datetime.date(ago=0) buy_date = self.rebalance_date + timedelta(days=1) if today == buy_date: #print(BUY DATE: , buy_date)  self.rebalance_buy() def notify_timer(self, timer, when, *args, **kwargs): if len(self.d_with_len) >= self.p.num_positions: self.rebalance_sell() def rebalance_sell(self): self.rebalance_date = self.data.datetime.date(ago=0) self.rankings = list(self.d_with_len) self.rankings.sort(key=lambda s: self.inds[s][momentum][0], reverse=True) for i, d in enumerate(self.rankings): if self.getposition(d).size != 0: if i >= self.p.num_positions: self.close(d, ticker=d.p.name) for o in self.orders[d]: if o and o.status == o.Accepted and / (o.getordername() == Stop or o.getordername() == Limit): self.cancel(o) # Rank according to momentum and return stock list  # Buy stocks with remaining cash  def rebalance_buy(self): positions = 0 for d in self.datas: if self.getposition(d).size != 0: positions += 1 if positions < self.p.num_positions: pos_value = self.broker.get_cash() / (self.p.num_positions - positions) for i, d in enumerate(self.rankings[:self.p.num_positions]): if self.getposition(d).size == 0 and / not math.isnan(self.inds[d][momentum][0]) > 0 and / pos_value > d.close[0]: buy_size = pos_value // d.close[0] buy_order = self.buy(d, size=buy_size, transmit=False, ticker=d.p.name) if self.p.use_atr: sell_price = d.close[0] + self.inds[d][atr][0] * self.p.atr_factor * self.p.rrr stop_price = d.close[0] - self.inds[d][atr][0] * self.p.atr_factor stop_loss = (self.inds[d][atr][0] * self.p.atr_factor) / d.close[0] else: sell_price = (1.0 + self.p.stop_loss * self.p.rrr) * d.close[0] stop_price = (1.0 - self.p.stop_loss) * d.close[0] stop_loss = self.p.stop_loss sell_order = self.sell(d, price=sell_price, size=buy_order.size, exectype=bt.Order.Limit, transmit=False, parent=buy_order, ticker=d.p.name) stop_order = self.sell(d, price=stop_price, size=buy_order.size, exectype=bt.Order.Stop, transmit=True, parent=buy_order, ticker=d.p.name) self.orders[d].append(sell_order) self.orders[d].append(stop_order) def stop(self): self.ending_value = round(self.broker.get_value(), 2) self.PnL = round(self.ending_value - startcash, 2) if __name__ == __main__: startcash = 10000 cerebro = bt.Cerebro(stdstats=False, optreturn=False) # Add Benchmark (datas[0])  benchmark = get_security_data(BENCHMARK_TICKER, START, END) benchdata = bt.feeds.PandasData(dataname=benchmark, name=SPY, plot=False) cerebro.adddata(benchdata) # Add Securities (datas[1:])  tickers = get_sp500_tickers() securities = get_securities_data(tickers, START_DATE, END_DATE) # Add securities as datas1:  for ticker, data in securities.groupby(level=0): if len(data) < MINIMUM_PERIOD: print(fSkipping: ticker {ticker} with length{len(data)} /  does not meet the minimum length of {MINIMUM_PERIOD}.) continue print(fAdding ticker: {ticker}.) d = bt.feeds.PandasData(dataname=data.droplevel(level=0), name=ticker, plot=False) d.plotinfo.plotmaster = benchdata d.plotinfo.plotlinelabels = True cerebro.adddata(d) print(Starting Portfolio Value: %.2f % cerebro.broker.getvalue()) # Add Strategy  if USE_ATR: cerebro.optstrategy(Strategy, rrr=(1, 2, 3, 4), atr_factor=(1, 2, 3, 4, 5)) else: cerebro.optstrategy(Strategy, rrr=(1, 2, 3, 4), stop_loss=(0.05, 0.10, 0.15, 0.20, 0.25)) # Add observers & analyzers  cerebro.addobserver(bt.observers.CashValue) cerebro.addobserver(bt.observers.Benchmark, data=benchdata, _doprenext=True, timeframe=bt.TimeFrame.NoTimeFrame) cerebro.addanalyzer(bt.analyzers.Returns) cerebro.addanalyzer(bt.analyzers.DrawDown) cerebro.addobserver(bt.observers.Trades) cerebro.addobserver(bt.observers.BuySell) # Analyze the trades  cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name=trades) # Run optimization  opt_results = cerebro.run(tradehistory=False) # Generate results list  final_results_list = [] for run in opt_results: for strategy in run: value = strategy.ending_value PnL = strategy.PnL if USE_ATR: stop_loss = strategy.p.atr_factor else: stop_loss = strategy.p.stop_loss rrr = strategy.p.rrr trades = strategy.analyzers.trades.get_analysis() total_trades = trades.total.closed total_won = trades.won.total perc_win = total_won / total_trades drawdown = strategy.analyzers.drawdown.get_analysis()[max][drawdown] final_results_list.append([rrr, perc_win, stop_loss, PnL, drawdown]) print(fStrategy Total Return: {strategy.analyzers.returns.get_analysis()[rtot]}) #Sort Results List  by_PnL = sorted(final_results_list, key=lambda x: x[3], reverse=True) by_win = sorted(final_results_list, key=lambda x: x[1], reverse=True) #Print results  print(Results: Ordered by Profit:) for result in by_PnL: print(| RRR | Win% | Stop | PnL | Drawdown|) print(| {} | {}% | {} | {} | {}.format( result[0], round(result[1], 2), result[2], round(result[3], 2), round(result[4], 2))) print(Results: Ordered by Win%:) for result in by_win: print(| RRR | Win% | Stop | PnL | Drawdown|) print(| {} | {}% | {} | {} | {}.format( result[0], round(result[1], 2), result[2], round(result[3], 2), round(result[4], 2))) 

Subscribe to Analyzing Alpha

Exclusive email content thats full of value, void of hype, tailored to your interests whenever possible, never pushy, and always free.

Reach out

Find us at the office

Gieser- Madigan street no. 4, 89728 Tokyo, Japan

Give us a ring

Danyelle Malanche
+96 551 917 434
Mon - Fri, 10:00-17:00

Tell us about you