Coding
Quant Python

A Paper Sharpe After Costs

Difficulty

Asked at D.E. Shaw, Two Sigma

Theory: Quant Python That Survives Review

Read the problem, hints and solution here. The editor needs a bigger screen: open this page on a laptop to write and run your code.

A backtest Sharpe ratio is a paper number until costs come out of it. "How do you adjust a backtest for transaction costs" is asked in researcher interviews because a fast-turnover signal that looks excellent gross is often nothing after fees, and because the first version most people write earns today's return on a position chosen after seeing it.

Implement net_backtest(returns_bp, positions, cost_bp).

  • returns_bp: the instrument's return on each day, in integer basis points.
  • positions: the target position at the close of each day, in units, possibly negative (short). Same length as returns_bp.
  • cost_bp: a one-way trading cost in basis points of the notional traded. One unit has a price of 1, so moving from \( p_{t-1} \) to \( p_t \) costs \( |p_t - p_{t-1}| \times cost\_bp \).

The book starts flat: the position before day 0 is 0, so the first trade is from 0 to positions[0] and is charged like any other. Nothing is closed out after the last day.

On day \( t \) the position chosen yesterday earns today's return: the gross P&L of day \( t \) is \( p_{t-1} \times r_t \), and day 0 earns nothing.

Return (gross_pnl_bp, cost_bp_total, net_pnl_bp, turnover_units):

  • gross_pnl_bp is \( \sum_{t \ge 1} p_{t-1} \, r_t \), in unit basis points: 3 units earning 10 basis points contribute 30.
  • turnover_units is \( \sum_{t \ge 0} |p_t - p_{t-1}| \) with \( p_{-1} = 0 \).
  • cost_bp_total is turnover_units * cost_bp, in the same units as the gross.
  • net_pnl_bp is gross_pnl_bp - cost_bp_total.

All four are integers. The Sharpe ratio after costs is the next step, but it needs a mean and a standard deviation, which are floats; the tests check the four integers and leave the Sharpe to you.

Examples

net_backtest([5, 20, -30, 40], [1, -1, 1, 0], 5)
# (90, 30, 60, 6)

net_backtest([10, -5, 20], [2, 2, 2], 10)
# (30, 20, 10, 2)

In the first, the long chosen on day 0 earns the 20 of day 1, the short earns the -30 of day 2 and the long earns the 40 of day 3; six units traded cost 5 each. In the second, buy and hold trades once and never earns the 10 of day 0.

Constraints

  • 0 <= len(returns_bp) <= 5 * 10^5; empty lists return (0, 0, 0, 0).
  • Positions and returns are integers of either sign. cost_bp >= 0.
  • The wrong answer that looks right pairs \( p_t \) with \( r_t \): the position sees the return it is paid for. A hidden test feeds a position that is the sign of the same day's return; the wrong formula makes it a perfect oracle.
  • The hidden performance case has 500,000 days. One pass carrying the previous position is enough.
Terms used in this problem (5)
position:
What you hold in an instrument: what you bought minus what you sold. Above zero you are long and gain when the price rises. Below zero you are short.
basis point:
One hundredth of one percent. A price 25 basis points higher is 0.25 percent higher.
notional:
The value of an order or a position in money: quantity times price.
P&L:
Profit and loss: what a position has made or lost. It is realised when the position is closed and unrealised while it is open.
Sharpe ratio:
The average return divided by the standard deviation of returns: return per unit of risk.
Rate this problem
Language: Python 3.12net_backtest
Rate this problem