Markets: A Rigorous Course

Exercise · m00_trader_tax.py

Tax on a mixed trading year, and F&O turnover

Implement every function, then press Run tests.

A deliberately simplified model of an Indian resident individual's tax year. It encodes only the rules stated in the notes; it is not tax advice.

Rules to encode

  1. Capital gains (listed equity, STT paid). Short-term loss offsets short-term gain, then long-term gain. Long-term loss offsets only long-term gain. Unabsorbed capital losses are carried forward.
  2. Business income. Speculative (intraday) and non-speculative (F&O) results are computed separately. A non-speculative loss may be set off against speculative profit; a speculative loss may not be set off against anything except speculative profit, and is carried forward.
  3. A non-speculative loss left after step 2 is set off against capital gains in the order rates["setoff_order"] (default ("stcg", "ltcg")), but never against salary. What remains is carried forward.
  4. Tax = slab tax on (salary + positive business income) + stcg_rate * net STCG + ltcg_rate * max(0, net LTCG - ltcg_exemption). Ignored: rebate, surcharge, cess, use of unexhausted basic exemption against special-rate gains, brought-forward losses, other deductions.

rates dict keys: "slabs" (list of (upper_limit, rate), last upper = inf), "stcg_rate", "ltcg_rate", "ltcg_exemption", optional "setoff_order".

Run locally

From the repository root, with the exercises environment installed:

cd exercises && .venv/bin/python -m pytest modules/M00-market-on-ramp/tests/test_m00_trader_tax.py

Tests (7)

  • test_slab_tax
  • test_turnover
  • test_notes_worked_example
  • test_fno_loss_offsets_speculative_then_capital_gains_not_salary
  • test_speculative_loss_is_ring_fenced
  • test_capital_loss_rules
  • test_setoff_order_parameter

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

not run yet

Not saved: sign up to keep your code
"""Exercise: tax on a mixed trading year, and F&O turnover (notes §0.7).

Implement every function, then run from the repo root:

    cd exercises && .venv/bin/python -m pytest modules/M00-market-on-ramp/tests/test_m00_trader_tax.py

A deliberately simplified model of an Indian resident individual's tax year.
It encodes only the rules stated in the notes; it is not tax advice.

Rules to encode
1. Capital gains (listed equity, STT paid). Short-term loss offsets short-term
   gain, then long-term gain. Long-term loss offsets only long-term gain.
   Unabsorbed capital losses are carried forward.
2. Business income. Speculative (intraday) and non-speculative (F&O) results
   are computed separately. A non-speculative loss may be set off against
   speculative profit; a speculative loss may not be set off against anything
   except speculative profit, and is carried forward.
3. A non-speculative loss left after step 2 is set off against capital gains in
   the order rates["setoff_order"] (default ("stcg", "ltcg")), but never
   against salary. What remains is carried forward.
4. Tax = slab tax on (salary + positive business income)
       + stcg_rate * net STCG + ltcg_rate * max(0, net LTCG - ltcg_exemption).
   Ignored: rebate, surcharge, cess, use of unexhausted basic exemption against
   special-rate gains, brought-forward losses, other deductions.

rates dict keys: "slabs" (list of (upper_limit, rate), last upper = inf),
"stcg_rate", "ltcg_rate", "ltcg_exemption", optional "setoff_order".
"""

from __future__ import annotations

import math
from typing import Sequence


def slab_tax(income: float, slabs: Sequence[tuple[float, float]]) -> float:
    """Progressive tax: each slice of income between consecutive upper limits
    is taxed at that slice's rate. slabs = [(400000, 0.0), (800000, 0.05), ..., (inf, 0.30)].
    Income <= 0 gives 0.
    """
    raise NotImplementedError


def fno_turnover(trade_pnls: Sequence[float], option_sell_premiums: Sequence[float] = (), include_premium: bool = False) -> float:
    """Turnover for tax-audit purposes as described in the notes: sum of
    |profit or loss| over all closed trades. With include_premium=True, also add
    the premium received on options sold (the older reading, for comparison)."""
    raise NotImplementedError


def tax_mixed_year(salary: float, stcg: float, ltcg: float, intraday: float, fno: float, rates: dict) -> dict:
    """Apply rules 1-4 of the module docstring. Return a dict with keys
    "slab_income", "business_income", "stcg_taxable", "ltcg_taxable" (after the
    exemption), "slab_tax", "stcg_tax", "ltcg_tax", "total_tax",
    "cf_speculative", "cf_business", "cf_stcl", "cf_ltcl".
    """
    raise NotImplementedError


NEW_REGIME_2026 = {
    "slabs": [
        (400_000, 0.00),
        (800_000, 0.05),
        (1_200_000, 0.10),
        (1_600_000, 0.15),
        (2_000_000, 0.20),
        (2_400_000, 0.25),
        (math.inf, 0.30),
    ],
    "stcg_rate": 0.20,
    "ltcg_rate": 0.125,
    "ltcg_exemption": 125_000,
}

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.