As software developers, while writing trading algorithms, it is beneficial to know whether our changes increase or decrease the output in a run.
Consider an example:
1. Seeing 200% returns over two years, great.
2. Make a change.
3. Now seeing only 180% returns.
4 Throw away that change and think in another direction.
5. You actually made 5% more per trade.
So what happened? How can that be?
The problem is that your change seemed bad yet actually resulted in an increase in profit per dollar transacted for stocks (amount risked, or put into play).
To quote someone here recently, I don't want to be a party pooper. And yet, the fact of the matter is, you can't trust that returns chart.
The returns calculation employs a philosophy that all of your starting capital is at risk as soon as you open the account, and that's all that is ever at risk.
It calculates returns based on starting capital regardless of how much you actually activated (even if you spent very little of it or went negative, into margin, borrowing from the broker). To be fair, that is apparently common in the industry. Meanwhile in developing code, this will help you see straight.
In the example, the new code made different trades based on the change and those trades were overall more profitable, it's just that the amount put to work on each stock was lower. You spent less. You made more for each dollar. That's the goal, right?
PvR stands for Profit vs. Risk. It calculates returns based only on the amount put to work and as a result, is a reliable measure of code merit.
With PvR, while you are writing code, you no longer need to be concerned with trying to make sure you are spending all of the starting capital. You can worry about that later, closer to the time you're ready to go live, or whenever you decide to focus on it. The PvR metric lets you stay focused.
It also means you no longer need to be locked into a cage-match battle with margin, leverage. Doesn't matter, for now. PvR only cares about profit per dollar put to work. You can start out with $100K and spend just $10K or another time $10M, PvR will always give you a deterministic value you can rely on that measures your code merit.
By the same token, with no changes in the code, whether your starting capital field in the backtester is set at $10 or $10M, if you make the same trades, the chart will vary widely while PvR will be the same. You can always trust it.
Try it. Clone below, run the backtest, note the ending PvR, then change the starting capital value and run again. The main chart returns will be very different, however PvR will be the same.
There are three sets of PvR code in this post:
1. Minimal amount of code for backtests
2. Full code for backtests
3. Research/Notebook code, experimental
Below is a minimal amount of code for recording PvR. For best speed, replace c. with context. and remove c = context, then move the initialization to def initialize.
def handle_data(context, data):
pvr(context, data)
def pvr(context, data):
''' Minimal custom chart of profit_vs_risk returns
'''
c = context # Brevity, readability
if 'pvr' not in c:
# For real money, you can modify this to total cash input minus any withdrawals
manual_cash = c.portfolio.starting_cash
c.pvr = {
'chart_pvr' : 1,
'chart_cash_low' : 1,
'chart_max_shrt' : 1,
'chart_max_risk' : 1,
'start' : manual_cash,
'cash_low' : manual_cash,
'max_shrt' : 0,
'max_risk' : 0,
}
c.pvr['cash_low'] = min(c.pvr['cash_low'], c.portfolio.cash)
c.pvr['max_shrt'] = max(c.pvr['max_shrt'], abs(sum([z.amount * z.last_sale_price for s, z in c.portfolio.positions.items() if z.amount < 0])))
c.pvr['max_risk'] = max(0, c.pvr['max_risk'], c.pvr['start'] - c.pvr['cash_low'], c.pvr['max_shrt'])
# Profit_vs_Risk returns based on max amount actually invested, risked, long or short
if c.pvr['max_risk'] != 0: # Avoid zero-divide
if c.pvr['chart_pvr']: record(PvR = 100 * (c.portfolio.portfolio_value - c.pvr['start']) / c.pvr['max_risk'])
if c.pvr['chart_cash_low']: record(CashLow = c.pvr['cash_low'])
if c.pvr['chart_max_shrt']: record(MxShrt = c.pvr['max_shrt'])
if c.pvr['chart_max_risk']: record(MxRisk = c.pvr['max_risk'])