Asked at D.E. Shaw, Two Sigma
Theory: Quant Python That Survives ReviewRead 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.
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.
0 <= len(returns_bp) <= 5 * 10^5; empty lists return (0, 0, 0, 0).cost_bp >= 0.