Exercise · m06b_nav.py
NAV: pricing a unit, daily fee accrual, stale-price wealth transfers and swing pricing
not run yet
Not saved: sign up to keep your code"""M06B · NAV: pricing a unit, daily fee accrual, stale-price wealth transfers and swing pricing.
Exercise stubs. Replace each ``raise NotImplementedError`` with your code, then run the tests in
../tests. See notes.md §6B.2: the NAV (6B.1) with the daily fee accrual (6B.2), Proposition 6B.2.1 on
dealing at a stale NAV (6B.3)–(6B.4), Proposition 6B.2.2 on swing pricing (6B.5), and Worked examples
6B.1–6B.2. §6B.10 uses swing pricing again as the fix for runs.
Conventions
-----------
* ``nav`` : net asset value per unit (₹), (assets − liabilities) / units.
* ``units`` : units outstanding before a deal (N).
* ``ter`` : expense ratio c, a fraction per year (0.008 = 0.80%), accrued daily on net assets.
* A deal at price ``p`` when the fair NAV is ``v``: a subscription of S rupees issues S/p units; a
redemption of u units pays u·p rupees. Wealth changes are measured against the fair NAV v.
* ``cost_rate``: proportional cost k the fund pays when it buys or sells securities to meet a flow
(spread plus impact plus taxes), per rupee of securities traded.
"""
from __future__ import annotations
import numpy as np
def nav_per_unit(assets: float, liabilities: float, units: float) -> float:
"""NAV = (A − L) / N (6B.1). Raise ValueError if units <= 0."""
raise NotImplementedError
def daily_fee_accrual(net_assets: float, ter: float, days_in_year: int = 365) -> float:
"""One day's expense accrual E = (c/365) · NA_pre (6B.2), in rupees."""
raise NotImplementedError
def nav_path_with_fees(nav0: float, daily_gross_returns, ter: float, days_in_year: int = 365) -> np.ndarray:
"""NAV path when the portfolio earns ``daily_gross_returns`` and the fee accrues each day (6B.2).
Day t: net assets before the charge NA_pre = NAV_{t−1}(1 + r_t) per unit; the accrual is
NA_pre · c/365, so NAV_t = NAV_{t−1}(1 + r_t)(1 − c/365). Return [NAV_0, NAV_1, …, NAV_n].
With zero gross returns for a year, NAV_365 = NAV_0 (1 − c/365)^365 ≈ NAV_0 e^{−c}: the NAV you see
is already net of the expense ratio.
"""
raise NotImplementedError
def nav_after_deal(nav_fair: float, deal_price: float, units: float, units_dealt: float) -> float:
"""Fair NAV after ``units_dealt`` = d units (positive issued, negative redeemed) are dealt at price p
while the portfolio is worth v per unit: v' = (N v + d p)/(N + d), so v' − v = d (p − v)/(N + d).
Proposition 6B.2.1(a): p = v leaves the NAV unchanged. Raise ValueError if N + d <= 0."""
raise NotImplementedError
def stale_purchase_transfer(nav_fair: float, nav_used: float, units: float, amount: float) -> tuple[float, float]:
"""A buyer invests S = ``amount`` at the stale NAV v_s = ``nav_used`` when the fair NAV is
v = v_s(1 + ε) = ``nav_fair``; the N = ``units`` existing units belong to the other holders.
Return (buyer's gain, existing holders' total change), in rupees against fair value. By
Proposition 6B.2.1(b) the buyer gains T_buy = S ε/(1 + f) with f = S/(N v_s) (6B.3), and the existing
holders lose exactly that. (Equivalently: the buyer gets d = S/v_s units worth v' each, with v' from
``nav_after_deal``.)
"""
raise NotImplementedError
def stale_redemption_transfer(nav_fair: float, nav_used: float, units: float, units_redeemed: float) -> tuple[float, float]:
"""u = ``units_redeemed`` of the N = ``units`` outstanding are redeemed at the stale NAV v_s while the
fair NAV is v = v_s(1 + ε). Return (redeemer's gain, remaining holders' total change):
T_red = −R ε with R = u v_s (6B.4), and the remaining holders lose exactly T_red."""
raise NotImplementedError
def swing_price(nav: float, net_flow: float, fund_value: float, swing_factor: float, threshold: float = 0.0) -> float:
"""Swung dealing price (6B.5). ``net_flow`` is the day's net subscriptions (₹, negative for net
redemptions). If |net_flow| > threshold · fund_value the price is NAV(1 + s) for net subscriptions and
NAV(1 − s) for net redemptions; otherwise (and when net_flow is 0) it is the NAV."""
raise NotImplementedError
def nav_after_flow_with_costs(nav: float, units: float, units_dealt: float, cost_rate: float,
swing_factor: float = 0.0) -> float:
"""NAV per unit after a flow of d = ``units_dealt`` units that the fund meets by trading securities
at proportional cost k = ``cost_rate`` (Proposition 6B.2.2).
The deal price is p = nav(1 + s) for subscriptions (d > 0) and nav(1 − s) for redemptions (d < 0).
* Subscription: the fund receives d p and buys securities worth d p/(1 + k).
* Redemption: the fund pays R = |d| p and sells securities with marked value X, X(1 − k) = R.
New NAV = (N nav + change in securities)/(N + d). With s = k it equals nav; with s = 0 a redemption
costs the remaining holders R k/(1 − k). Return nav if d = 0; raise ValueError if N + d <= 0.
"""
raise NotImplementedError
No runs yet
The stub's functions raise
NotImplementedError until you write them. Fill in one, press
Run tests, and watch its tests turn green.Reference solution
Available after your first run, with an account approved for code runs.