Tax rules implemented by taxlots, with hand calculations

All section numbers refer to the Income-tax Act, 1961 as amended up to the Finance Act 2025. Every worked example below is asserted by a test (file and test name are given), so the numbers in this document and the code cannot drift apart silently.

"FY" is the financial year (1 April – 31 March); "AY" is the following assessment year. The rule set is chosen by the sale date (the date of transfer), never by the purchase date, except where a rule says otherwise (sec 50AA, grandfathering).


1. Lot matching — FIFO per ISIN per account

Law. Sec 45(2A), Explanation: for securities held in a depository (demat), "the cost of acquisition and the period of holding of any securities shall be determined on the basis of the first-in-first-out method". CBDT Circular 768 (24-Jun-1998) applies this per demat account. Specific identification is only possible for holdings that are not in demat form (e.g. mutual-fund units held in a statement-of-account folio).

Implementation. One FIFO queue per (account, ISIN). Specific-ID is configured in taxlots.toml ([[specific_id]]) and is refused unless the account is listed in [matching] non_demat_accounts.

Hand calc (test_engine.py::test_fifo_across_partial_lots_with_charge_allocation):

Trade Qty Price Charges
B1 buy 10-Jan-2020 100 10 10
B2 buy 10-Feb-2020 50 12 6
S1 sell 1-Mar-2022 120 15 12

2. Charges

Law. Sec 48(i): "expenditure incurred wholly and exclusively in connection with such transfer" is deductible; costs incurred to acquire the asset form part of the cost of acquisition. STT is not deductible (sec 48, proviso inserted by Finance (No.2) Act 2004).

Implementation. Buy charges are added to the lot's cost and follow it through partial sales pro-rata by quantity; sell charges are allocated pro-rata by quantity across the lots a sale consumes. Put brokerage, exchange fees, GST and stamp duty in the charges column — leave STT out.

3. Holding period — sec 2(42A)

An asset is short-term if held for not more than N months immediately preceding the date of transfer. taxlots excludes the purchase date and includes the sale date, so for N = 12 an asset bought on 15-Jan-2023 is long-term on a sale on 16-Jan-2024, not on 15-Jan-2024 (test_rules.py::test_equity_twelve_months_is_strictly_more_than). Month-ends clamp: bought 29-Feb-2024 → 12 months end 28-Feb-2025.

Asset type Sale before 23-Jul-2024 Sale on/after 23-Jul-2024
listed_equity, equity_mf, reit_invit 12 months 12 months
gold_etf (listed MF units, non-equity) 36 months 12 months (listed security)
debt_mf bought before 1-Apr-2023 36 months 24 months
foreign_equity (unlisted in India) 24 months 24 months
debt_mf bought on/after 1-Apr-2023 always short-term (sec 50AA) always short-term

Finance (No.2) Act 2024 replaced the proviso to sec 2(42A) for transfers on or after 23-Jul-2024: 12 months for all securities listed on a recognised stock exchange in India (which includes listed ETF units), 24 months for everything else.

Deviation from the project spec, deliberately. The spec groups "foreign / gold / debt pre-2023" as 36 → 24 months. That is right for debt funds but not for the other two: foreign shares are shares not listed in India, which have used the 24-month threshold since FY 2016-17, and an exchange-traded gold ETF is a listed security and so uses 12 months after 23-Jul-2024.

Hand calcs (test_rules.py): * Gold ETF bought 10-Feb-2021: 36 months end 10-Feb-2024 → sale on 10-Feb-2024 is short-term, on 11-Feb-2024 long-term. * Debt MF bought 1-Dec-2021, sold 11-Nov-2024 (post-switch, 24 months): long-term from 2-Dec-2023 → long-term. * Foreign share bought 15-Jan-2021: long-term from 16-Jan-2023.

4. Sec 50AA — specified mutual funds

Units of a "specified mutual fund" acquired on or after 1-Apr-2023 are deemed short-term regardless of holding period and taxed at slab rates (inserted by Finance Act 2023).

Implementation. debt_mf bought on/after 1-Apr-2023 → 50AA forever. gold_etf bought on/after 1-Apr-2023 → 50AA only when sold on or before 31-Mar-2025. The pre/post-1-Apr-2023 status is derived from each lot's own acquisition date (after merger carry-over), so the asset master needs no separate field for it, and a single fund can hold lots on both sides.

Hand calcs (examples, taxlots gains): * GOLDBEES lot G-02, bought 15-May-2023 @ 48, 500 units sold 10-Feb-2025 @ 62 → 50AA STCG = 31,000 − 24,000 = 7,000 (slab). * GOLDBEES lot G-04, bought 20-Apr-2024 @ 55, 200 units sold 2-Jun-2025 @ 70 (FY 2025-26): not specified; long-term from 21-Apr-2025 → sec 112 @ 12.5 % on 14,000 − 11,000 = 3,000 → tax 375 (test_summary.py::test_gold_etf_fy_2025_26_is_long_term_at_12_5).

5. Rates by sale date

Section What Before 23-Jul-2024 On/after 23-Jul-2024
10(38) LTCG on STT-paid equity, sold ≤ 31-Mar-2018 exempt —
111A STCG on STT-paid equity, equity MF, business-trust units 15 % 20 %
112A LTCG on the same 10 % above ₹1,00,000 12.5 % above ₹1,25,000
112 other LTCG (debt MF pre-2023, gold, foreign) 20 % with indexation 12.5 % without indexation
— other STCG, 50AA slab slab

Source: Finance (No.2) Act 2024, amending secs 111A, 112, 112A and 48 for transfers on or after 23-Jul-2024. Tested in test_rules.py::test_equity_rate_switch_on_23_july_2024 (sale on 22-Jul-2024 → 15 %, on 23-Jul-2024 → 20 %).

Sec 10(38) (sales up to 31-Mar-2018) is supported so that old tradebooks replay correctly (test_summary.py::test_section_10_38_gain_is_exempt_and_not_set_off). Gains and losses from exempt sources are kept out of set-off.

The ₹1,25,000 exemption in FY 2024-25

Sec 112A as amended sets the threshold at ₹1,25,000 for the whole of FY 2024-25, applied to the aggregate of LTCG before and after 23-Jul-2024. The Act does not prescribe which portion absorbs the exemption, so taxlots applies it to the 12.5 % gains first (the tax-minimising choice; the ITR utility lets you allocate it).

Hand calc (test_summary.py::test_fy_2024_25_exemption_spans_both_rate_periods_highest_rate_first): 60,000 LTCG at 10 % (June 2024) + 1,00,000 at 12.5 % (Oct 2024). Exemption → 1,00,000 to the 12.5 % bucket, 25,000 to the 10 % bucket. Tax = (60,000 − 25,000) × 10 % = 3,500.

FY 2023-24 uses ₹1,00,000: 1,50,000 LTCG at 10 % → (1,50,000 − 1,00,000) × 10 % = 5,000.

6. Grandfathering — sec 55(2)(ac)

For equity shares, equity-MF units and business-trust units acquired on or before 31-Jan-2018 and taxed under sec 112A, the cost of acquisition is the higher of (a) actual cost and (b) the lower of the FMV on 31-Jan-2018 and the full value of consideration (sale value, before expenses). FMV = highest quoted price on 31-Jan-2018 (NAV for unlisted units). Indexation does not apply under 112A.

Hand calcs (test_engine.py::test_grandfathering_55_2_ac, one unit each):

Cost FMV Sale min(FMV, Sale) Cost used Gain
100 200 250 200 200 50
100 200 150 150 150 0
300 200 250 200 300 −50
100 200 80 80 100 −20

Rows 2 and 4 show the two intended effects: grandfathering can wipe out a gain but can never create an artificial loss, and it never reduces actual cost.

How FMV follows corporate actions. The asset master stores the FMV as quoted on 31-Jan-2018. taxlots stamps it onto every lot open at the end of 31-Jan-2018 and then scales it with any later split/merger. A split before 2018 needs no adjustment, because the quoted FMV is already post-split (test_split_before_2018_uses_post_split_fmv_as_quoted).

Example: INFY sale 1012 (10-Jun-2024, 150 @ 1,500; FY 2024-25, 10 % regime): * Lot 1002: 100 bought 15-Jun-2016 @ 950 → cost 95,000; FMV 1,180 × 100 = 1,18,000; sale 1,50,000. Cost used = max(95,000, min(1,18,000, 1,50,000)) = 1,18,000; gain 32,000. * Bonus lot (1:1 on 4-Sep-2018), 50 units: cost 0, acquired after 31-Jan-2018 → no grandfathering; gain 50 × 1,500 = 75,000. Total 1,07,000 at 10 %.

7. Corporate actions

Action Law Effect on lots
Split / consolidation not a transfer; cost apportioned qty × new/old, total cost unchanged, FMV ÷ ratio, acquisition date unchanged
Bonus sec 55(2)(aa)(iiia): cost nil; holding period from allotment date new lot of ⌊held × bonus/held⌋ units, cost 0, acquired on the allotment date; original lots untouched
Amalgamation / rename sec 47(vii) not a transfer; sec 49(2) cost carried; sec 2(42A) Expl. 1(b) holding period includes the old shares lots move to the new ISIN with qty × new/old, cost and acquisition date carried

Split example: HDFCBANK sale 1023 (test_split_after_2018_rescales_fmv). 50 bought 10-Mar-2017 @ 1,200 (cost 60,000), FMV 31-Jan-2018 = 1,990. Split 1:2 on 19-Sep-2019 → 100 shares, FMV per share 995. Sold 100 @ 1,650 on 20-Jan-2025: cost used = max(60,000, min(99,500, 1,65,000)) = 99,500; gain = 1,65,000 − 99,500 = 65,500 at 12.5 %.

Bonus trap (test_bonus_shares_sold_soon_after_allotment_are_short_term): 10 shares held since 2015, bonus 1:1 on 28-Oct-2024, all 20 sold on 15-Jan-2025 @ 150 (cost 100). The 10 originals are LTCG (500); the 10 bonus shares were acquired less than 12 months ago → STCG 111A at 20 % on the full 1,500.

Merger example: HDFC → HDFCBANK, sale I-02 (test_merger_carries_cost_and_acquisition_date). 25 HDFC bought 1-Aug-2016 @ 2,000 + 50 charges = 50,050; FMV 1,860. Merger 42:25 on 13-Jul-2023 → 42 HDFCBANK, FMV per share 1,860 × 25/42, total FMV 46,500. Sold 42 @ 1,700 on 5-Mar-2025 with 71.40 charges: cost used = max(50,050, min(46,500, 71,400)) = 50,050; gain = 71,400 − 71.40 − 50,050 = 21,278.60.

8. Indexation — sec 48, second proviso; CII

Indexed cost = cost × CII(year of transfer) / CII(year of acquisition). Available only for sec 112 LTCG on transfers before 23-Jul-2024. The CII table (base FY 2001-02 = 100, CBDT Notification 44/2017) is embedded in rules.py:

FY CII FY CII FY CII
2001-02 100 2010-11 167 2019-20 289
2002-03 105 2011-12 184 2020-21 301
2003-04 109 2012-13 200 2021-22 317
2004-05 113 2013-14 220 2022-23 331
2005-06 117 2014-15 240 2023-24 348
2006-07 122 2015-16 254 2024-25 363
2007-08 129 2016-17 264 2025-26 376
2008-09 137 2017-18 272
2009-10 148 2018-19 280

Source: CBDT notifications under sec 48 Explanation (v); FY 2024-25 = 363 (notified May 2024), FY 2025-26 = 376 (notified July 2025). Assets acquired before 1-Apr-2001 (where the FMV-on-1-Apr-2001 substitution applies) are out of scope and raise an error if indexation is needed.

Hand calc: CORPBOND-A sale C-05 (test_indexation_for_pre_switch_debt_fund). 1,000 units bought 15-Apr-2019 (FY 2019-20, CII 289) @ 21.50; sold 20-May-2024 (FY 2024-25, CII 363, before 23-Jul-2024) @ 28.40. Held > 36 months → LTCG sec 112 @ 20 %. Indexed cost = 21,500 × 363 / 289 = 27,005.19; gain = 28,400 − 27,005.19 = 1,394.81.

Foreign equity: AAPL sale F-02 (FY 2023-24). 10 bought 15-Jan-2021 (FY 2020-21, CII 301) at ₹9,500 + ₹250 charges = 95,250; sold 10-Aug-2023 (CII 348) at ₹14,800 less ₹300 charges. Indexed cost = 95,250 × 348/301 = 1,10,122.92; gain = 1,47,700 − 1,10,122.92 = 37,577.08.

9. Intraday = speculative business income — sec 43(5)

A purchase and sale of the same share on the same day without delivery is a speculative transaction (sec 43(5)); its result is business income, not capital gains. Speculative loss can be set off only against speculative income and carried forward for 4 AYs (sec 73(1), 73(4)).

Implementation. For each (account, ISIN, date) of listed_equity, intraday qty = min(bought, sold) that day. That quantity is taken from the day's buys and sells in order (charges pro-rata); the remainder flows into FIFO as delivery. Mutual funds and other assets are never treated as intraday.

Hand calc (test_intraday_is_separated_from_delivery): 20 shares held from 2020 @ 50. On 5-Nov-2024: buy 30 @ 100 (charges 3), sell 50 @ 110 (charges 5). * Intraday 30: 3,300 − 3,000 − (3 + 5 × 30/50) = 294 speculative income. * Delivery sale 20 @ 110, charges 2, FIFO from the 2020 lot: 2,200 − 2 − 1,000 = 1,198 LTCG.

10. Set-off and carry forward — secs 70, 71, 74

Losses must be set off in the year they arise; you cannot choose to carry a loss forward while gains exist. The 112A exemption applies to the gain that remains after set-off. taxlots therefore:

  1. nets gains and losses inside each bucket (as the ITR's Schedule CG does);
  2. applies current-year LTCL, then STCL, then brought-forward LTCL, then STCL;
  3. each loss goes to the highest-rate eligible bucket first, and to the part of a 112A bucket covered by the exemption only once every taxable gain is used up.

LTCL is applied before STCL because it has fewer eligible targets; with eligible sets nested like this, greedy highest-rate-first is optimal.

Hand calc 1 (test_stcl_goes_to_highest_rate_gains_first, FY 2024-25, slab 30 %): STCL 10,000 (111A); 50AA gain 4,000; 112A gain 2,00,000. STCL → 4,000 against 50AA (30 %), 6,000 against 112A (12.5 %). 112A taxable = 2,00,000 − 6,000 − 1,25,000 = 69,000 → tax 8,625; cess 4 % = 345; total 8,970.

Hand calc 2 (test_ltcl_cannot_reduce_stcg_and_is_carried_forward_8_years): STCG 50,000 at 20 % and LTCL 20,000. LTCL cannot touch STCG → tax 10,000 + 400 cess; LTCL 20,000 carried forward, usable until AY 2033-34 (FY 2024-25 = AY 2025-26, plus 8).

Hand calc 3 (test_ltcl_set_off_is_mandatory_even_against_exempt_112a_gain): 112A gain 1,00,000 (fully inside the exemption) and a debt-fund LTCL of 30,000. The loss must still be set off: 112A becomes 70,000, all exempt; tax 0; nothing is carried forward. (This is the well-known "wasted loss" — the tool reports it rather than hiding it.)

11. Tax estimate, surcharge and cess

Hand calc (test_surcharge_capped_at_15_percent_on_special_rate_gains, surcharge 25 %): 111A 1,00,000 × 20 % = 20,000 → surcharge capped 15 % = 3,000. 50AA 10,000 × 30 % = 3,000 → surcharge 25 % = 750. Cess = (23,000 + 3,750) × 4 % = 1,070.

12. Full worked example — FY 2024-25 from examples/

taxlots summary --fy 2024-25 (asserted by test_cli.py::test_summary_fy_2024_25_matches_hand_calculation):

Bucket Items Net
111A @15 % TATAMOTORS 1013: 200 × (980 − 900) 16,000.00
111A @20 % RELIANCE 1017 +5,000; TATAMOTORS 1021: 100 × (800 − 1,100) = −30,000 −25,000.00
50AA GOLDBEES G-05 7,000.00
112A @10 % INFY 1012 (32,000 + 75,000) 1,07,000.00
112A @12.5 % IRCTC 35,400; PPFAS 16,350; EMBASSY −1,000; SBIN 48,000; HDFCBANK 65,500; HDFCBANK (icici) 21,278.60 1,85,528.60
112 @20 % indexed CORPBOND-A C-05 1,394.81
112 @12.5 % CORPBOND-A C-07: 500 × (29 − 25) 2,000.00

Set-off of the 25,000 STCL, highest rate first: 7,000 → 50AA (30 %), 1,394.81 → 112 @20 %, 16,000 → 111A @15 %, the last 605.19 → a 12.5 % bucket.

Exemption 1,25,000 → 112A @12.5 %. Taxable: 112A @10 % 1,07,000; 112A @12.5 % 1,85,528.60 − 605.19 − 1,25,000 = 59,923.41; 112 @12.5 % 2,000. Intraday TCS: +300 at 30 %.

Tax = 10,700 + 7,490.43 + 250 + 90 = 18,530.43; cess 741.22; total 19,271.64.

13. Tax-gain harvesting

Goal: realise long-term 112A gains up to the unused exemption (default ₹1,25,000), re-buy, and so step up the cost basis for free. Two constraints from the law shape the algorithm:

So for each (account, ISIN) taxlots takes the longest prefix of lots that are long-term on the planned date and in profit, works out the gain per unit (with grandfathering), and greedily fills the budget, holdings with the highest gain per unit first (fewest units to trade), selling whole shares (0.001 units for mutual funds).

Hand calc (test_harvest.py::test_fills_budget_highest_gain_per_unit_first): budget 3,000 − 500 already realised = 2,500. EQ1: 5 @ 50, price 1,000 → 950/unit. EQ2 lots: 10 @ 100, 10 @ 150, 10 @ 190 (short-term), price 200 → 100/unit, then 50/unit. EQ1 first: ⌊2,500/950⌋ = 2 → 1,900, 600 left. EQ2 lot 1: ⌊600/100⌋ = 6 → 600. Remaining 0.

Example run (FY 2025-26, as of 31-Mar-2026, 23,000 already realised from ITC): INFY bonus 50 × 1,550 = 77,500; IRCTC 40 × (780 − 360) = 16,800; ITC 36 × (410 − 200) = 7,560; PPFAS 2.170 × (88 − 23.50) = 139.97. Total 1,01,999.97 of the 1,02,000 budget.

Limitations (deliberately out of scope)