Markets: A Rigorous Course

Exercise · m06b_nav.py

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.

Tests (8)

  • test_worked_example_6b1_nav
  • test_nav_is_net_of_the_fee
  • test_fair_deal_leaves_nav_unchanged
  • test_stale_purchase_formula_6b3
  • test_stale_transfer_matches_explicit_ledger
  • test_worked_example_6b2_debt_fund_redemption
  • test_swing_price_6b5
  • test_swing_factor_equal_to_cost_removes_dilution

Theory: M06B notes. Run with Ctrl/⌘+Enter.

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.

Create a free account

Reading is open to everyone. An account keeps your progress, answers, notes and code drafts, and gives you the study plan, the journals and the simulator. Running code needs an account approved for it.