Asset Allocation & Rebalancing
Level: 5 (Mastery — Cross-domain: math + quantitative finance + coding) Time limit: 90 minutes Total marks: 60
Instructions: Answer all three questions. Show full derivations. Where code is requested, write clean, runnable Python (NumPy/pandas allowed) and state complexity where asked. Use / for math.
Question 1 — Threshold Rebalancing & Portfolio Drift (20 marks)
A two-asset portfolio has a strategic target weight in equity, in bonds. The equity weight drifts to over time. A threshold rebalancing rule triggers a trade whenever (absolute band, ).
(a) Starting from a portfolio value at target weights, suppose over one period equity returns and bonds return . Compute the new equity weight and the drift . Does the threshold trigger? Show the exact fraction. (6)
(b) Prove the following general result: for a two-asset portfolio at target , after equity return and bond return , the post-return equity weight is Then derive the minimum equity outperformance (assuming fixed) that triggers an upper-band breach , expressed in closed form in terms of . (8)
(c) Write a Python function rebalance_threshold(weights, targets, tau) that returns the trade vector (fraction of total portfolio to buy/sell per asset) to restore targets when any asset breaches its band, else a zero vector. State its time complexity in terms of assets. (6)
Question 2 — Rupee-Cost Averaging vs Lump Sum (20 marks)
An investor contributes a fixed amount each period into a single fund whose price per unit at period is (for ).
(a) Show that the average cost per unit under rupee-cost averaging equals the harmonic mean of the prices: and prove using the AM–HM inequality that (the arithmetic mean of prices), with equality iff all are equal. (9)
(b) Given prices over 4 months with per month, compute the total units bought, the average cost per unit , and compare to the arithmetic mean price. State the final portfolio value at a closing price of . (7)
(c) Explain quantitatively (with the variance/dispersion argument) under what price-path condition rupee-cost averaging beats lump-sum investing, and when it underperforms. Give one concrete counter-example price path. (4)
Question 3 — Core–Satellite Optimisation & Tax-Efficient Rebalancing (20 marks)
A core–satellite portfolio holds a passive core (weight , expected return , volatility ) and an active satellite (weight , , ), with correlation .
(a) Derive the portfolio variance and find the core weight that minimises portfolio variance. Give the closed-form expression and evaluate numerically. (9)
(b) The satellite has drifted, requiring a sell that realises a gain. Short-term capital gains are taxed at , long-term at . A rebalance requires selling ₹200,000 of gains. Model the trade-off: rebalancing now (cost = tax) vs waiting (cost = expected tracking-error drift). If the tracking-error cost of not rebalancing is modelled as where = drift fraction and , and current drift is , determine whether to rebalance now (short-term tax) assuming the position becomes long-term in the next period. Show the numeric comparison. (7)
(c) Write pseudocode / Python for a tax-aware rebalancing decision: given per-lot cost basis, current price, holding period, and target trade size, choose lots to sell that minimise realised tax while meeting the trade size. Identify which classic algorithmic problem this resembles. (4)
Answer keyMark scheme & solutions
Question 1
(a) Initial values: equity , bond . After returns: equity ; bond . Total . (3 marks) Drift . (2 marks) Since , the threshold triggers. (1 mark)
(b) Proof. Post-return equity value ; bond value . Total . Dividing equity by total ( cancels): (3 marks)
Trigger condition . Let , . Then So , giving With fixed, minimum outperformance: (5 marks) Check with , , : . Any triggers; does — consistent with (a).
(c) (6 marks: 4 correct logic, 1 zero-vector case, 1 complexity)
import numpy as np
def rebalance_threshold(weights, targets, tau):
weights = np.asarray(weights, float)
targets = np.asarray(targets, float)
if np.any(np.abs(weights - targets) >= tau):
return targets - weights # buy/sell fractions to restore targets
return np.zeros_like(weights)Complexity: — one pass to test bands, one pass to compute the trade.
Question 2
(a) In period , units bought . Total units . Total invested . Average cost: (5 marks) By AM–HM inequality, for positive : , i.e. , with equality iff all equal (proof: AM–HM follows from AM–GM or Cauchy–Schwarz ). (4 marks)
(b) Units: , , , . Total units. (2) Total invested . . (2) Arithmetic mean price ; indeed . (1) Final value at price : . (2)
(c) RCA outperforms lump sum when prices are volatile / dispersed and mean-reverting or dip early — higher price dispersion widens the AM–HM gap, lowering average cost. It underperforms in a steadily rising market, where early lump-sum capital captures more upside (opportunity cost of un-invested cash dominates). (3) Counter-example: monotone rising path — lump sum at beats RCA. (1)
Question 3
(a) Portfolio variance: (3) Minimise: gives (3) Numerics: . . Numerator . Denominator . (3)
(b) Rebalance now (short-term): tax . (2) Wait one period → long-term tax , but incur tracking cost of holding the drift: . (3) Compare: rebalance now ; wait . Wait — waiting saves (the long-term tax saving of ₹10,000 far exceeds the ₹800 drift cost). (2)
(c) (4 marks)
def tax_aware_sell(lots, target_value, price, st_rate, lt_rate):
# lots: list of dicts {basis, qty, long_term(bool)}
# compute tax per rupee sold for each lot, sell cheapest-tax first
def tax_rate(lot): return lt_rate if lot['long_term'] else st_rate
def unit_tax(lot):
gain = max(price - lot['basis'], 0)
return tax_rate(lot) * gain / price # tax per rupee of proceeds
lots_sorted = sorted(lots, key=unit_tax) # greedy: lowest tax first
proceeds, tax = 0.0, 0.0
for lot in lots_sorted:
cap = lot['qty'] * price
take = min(cap, target_value - proceeds)
units = take / price
tax += tax_rate(lot) * max(price - lot['basis'],0) * units
proceeds += take
if proceeds >= target_value: break
return proceeds, taxThis resembles a fractional knapsack / greedy selection problem (sort by tax-per-proceeds ratio, fill greedily), which is optimal because lots are divisible.
[
{"claim":"Q1a equity weight after +25% equity, 0% bond from 0.6 target = 15/23",
"code":"w1=Rational(600000*1.25,1150000); result=(w1==Rational(15,23))"},
{"claim":"Q1a drift exceeds tau=0.05",
"code":"drift=Rational(15,23)-Rational(3,5); result=(drift>=Rational(1,20))"},
{"claim":"Q1b min outperformance ~0.2381 for wstar=0.6,tau=0.05,rB=0",
"code":"val=(0.65*0.4)/(0.6*0.35)-1; result=abs(val-0.238095)<1e-4"},
{"claim":"Q2b average cost = 40000/405 and less than 101.25",
"code":"c=40000/405; result=(abs(c-98.7654)<1e-3 and c<101.25)"},
{"claim":"Q2b final value at 110 = 44550",
"code":"U=10000/100+10000/80+10000/125+10000/100; result=(abs(U*110-44550)<1e-6)"},
{"claim":"Q3a min-variance core weight approx 0.9083",
"code":"num=0.0625-0.009; den=0.0144+0.0625-0.018; result=abs(num/den-0.90832)<1e-3"},
{"claim":"Q3b waiting (20800) cheaper than rebalancing now (30000)",
"code":"now=0.15*200000; wait=0.10*200000+500000*0.04**2; result=(wait<now and abs(wait-20800)<1e-6)"}
]