Skip to content

Commit 39de63e

Browse files
committed
add indexation index data for taxable debt fund gains computation
1 parent bca03f2 commit 39de63e

File tree

5 files changed

+114
-29
lines changed

5 files changed

+114
-29
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Changelog
22

3-
## 0.4.9 - WIP
3+
## 0.5.0 - 2021-07-02
44
- Support for calculating capital gains from detailed CAS statements
55

66

casparser/analysis/gains.py

Lines changed: 24 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from casparser.exceptions import IncompleteCASError
1414
from casparser.enums import FundType, GainType, TransactionType
1515
from casparser.types import CASParserDataType, TransactionDataType
16-
from .utils import nav_search
16+
from .utils import CII, get_fin_year, nav_search
1717

1818

1919
@dataclass
@@ -35,18 +35,9 @@ class Fund:
3535
isin: str
3636
type: str
3737

38-
def __le__(self, other: "Fund"):
39-
return self.name <= other.name
40-
41-
def __ge__(self, other: "Fund"):
42-
return self.name >= other.name
43-
4438
def __lt__(self, other: "Fund"):
4539
return self.name < other.name
4640

47-
def __gt__(self, other: "Fund"):
48-
return self.name > other.name
49-
5041

5142
@dataclass
5243
class GainEntry:
@@ -94,8 +85,16 @@ def fmv(self) -> Decimal:
9485
return self.buy_price
9586
return self._cached_nav * self.units
9687

88+
@property
89+
def index_ratio(self) -> Decimal:
90+
return Decimal(
91+
round(CII[get_fin_year(self.sell_date)] / CII[get_fin_year(self.buy_date)], 2)
92+
)
93+
9794
@property
9895
def coa(self) -> Decimal:
96+
if self.fund.type == FundType.DEBT.name:
97+
return Decimal(round(self.buy_price * self.index_ratio, 2))
9998
if self.buy_date < self.__cutoff_date:
10099
if self.sell_date < self.__sell_cutoff_date:
101100
return self.sell_price
@@ -108,6 +107,12 @@ def ltcg_taxable(self) -> Decimal:
108107
return Decimal(round(self.sell_price - self.coa, 2))
109108
return Decimal(0.0)
110109

110+
@property
111+
def stcg_taxable(self) -> Decimal:
112+
if self.gain_type == GainType.STCG:
113+
return Decimal(round(self.sell_price - self.coa, 2))
114+
return Decimal(0.0)
115+
111116
@property
112117
def ltcg(self) -> Decimal:
113118
if self.gain_type == GainType.LTCG:
@@ -194,19 +199,6 @@ def merge_transactions(self):
194199
merged_transactions[dt].amount += txn["amount"]
195200
return merged_transactions
196201

197-
@staticmethod
198-
def get_fin_year(dt: date):
199-
"""Get financial year representation."""
200-
if dt.month > 3:
201-
year1, year2 = dt.year, dt.year + 1
202-
else:
203-
year1, year2 = dt.year - 1, dt.year
204-
205-
if year1 % 100 != 99:
206-
year2 %= 100
207-
208-
return f"FY{year1}-{year2}"
209-
210202
def process(self):
211203
self.gains = []
212204
for dt in sorted(self._merged_transactions.keys()):
@@ -221,7 +213,7 @@ def buy(self, txn_date: date, quantity: Decimal, nav: Decimal, tax: Decimal):
221213
self.transactions.append((txn_date, quantity, nav, tax))
222214

223215
def sell(self, sell_date: date, quantity: Decimal, nav: Decimal, tax: Decimal):
224-
fin_year = self.get_fin_year(sell_date)
216+
fin_year = get_fin_year(sell_date)
225217
original_quantity = abs(quantity)
226218
pending_units = original_quantity
227219
while pending_units > 0:
@@ -291,17 +283,20 @@ def get_summary(self):
291283
"""Calculate capital gains summary"""
292284
summary = []
293285
for (fy, fund), txns in itertools.groupby(self.gains, key=lambda x: (x.fy, x.fund)):
294-
ltcg = stcg = ltcg_taxable = Decimal(0.0)
286+
ltcg = stcg = ltcg_taxable = stcg_taxable = Decimal(0.0)
295287
for txn in txns:
296288
ltcg += txn.ltcg
297289
stcg += txn.stcg
298290
ltcg_taxable += txn.ltcg_taxable
299-
summary.append([fy, fund.name, fund.isin, fund.type, ltcg, ltcg_taxable, stcg])
291+
stcg_taxable += txn.stcg_taxable
292+
summary.append(
293+
[fy, fund.name, fund.isin, fund.type, ltcg, ltcg_taxable, stcg, stcg_taxable]
294+
)
300295
return summary
301296

302297
def get_summary_csv_data(self) -> str:
303298
"""Return summary data as a csv string."""
304-
headers = ["FY", "Fund", "ISIN", "Type", "LTCG", "LTCG(Taxable)", "STCG"]
299+
headers = ["FY", "Fund", "ISIN", "Type", "LTCG", "LTCG(Taxable)", "STCG", "STCG(Taxable)"]
305300
with io.StringIO() as csv_fp:
306301
writer = csv.writer(csv_fp)
307302
writer.writerow(headers)
@@ -329,6 +324,7 @@ def get_gains_csv_data(self) -> str:
329324
"LTCG",
330325
"LTCG Taxable",
331326
"STCG",
327+
"STCG Taxable",
332328
]
333329
with io.StringIO() as csv_fp:
334330
writer = csv.writer(csv_fp)
@@ -351,6 +347,7 @@ def get_gains_csv_data(self) -> str:
351347
gain.ltcg,
352348
gain.ltcg_taxable,
353349
gain.stcg,
350+
gain.stcg_taxable,
354351
]
355352
)
356353
csv_fp.seek(0)

casparser/analysis/utils.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,68 @@
1+
from collections import UserDict
2+
from datetime import date
13
from decimal import Decimal
4+
import re
25
from typing import Optional
36

47
from casparser_isin import MFISINDb
58

9+
CII_DATA = {
10+
"FY2001-02": 100,
11+
"FY2002-03": 105,
12+
"FY2003-04": 109,
13+
"FY2004-05": 113,
14+
"FY2005-06": 117,
15+
"FY2006-07": 122,
16+
"FY2007-08": 129,
17+
"FY2008-09": 137,
18+
"FY2009-10": 148,
19+
"FY2010-11": 167,
20+
"FY2011-12": 184,
21+
"FY2012-13": 200,
22+
"FY2013-14": 220,
23+
"FY2014-15": 240,
24+
"FY2015-16": 254,
25+
"FY2016-17": 264,
26+
"FY2017-18": 272,
27+
"FY2018-19": 280,
28+
"FY2019-20": 289,
29+
"FY2020-21": 301,
30+
}
31+
32+
33+
class _CII(UserDict):
34+
def __init__(self, *args, **kwargs):
35+
super().__init__(*args, **kwargs)
36+
self.years = list(sorted(self.data.keys()))
37+
self._min_year = self.years[0]
38+
self._max_year = self.years[-1]
39+
40+
def __missing__(self, key):
41+
if not re.search(r"FY\d{4}-\d{2}", key):
42+
raise ValueError("Invalid FY year format.")
43+
elif key <= self._min_year:
44+
return self.data[self._min_year]
45+
elif key >= self._max_year:
46+
return self.data[self._max_year]
47+
raise KeyError(key)
48+
49+
50+
CII = _CII(CII_DATA)
51+
652

753
def nav_search(isin: str) -> Optional[Decimal]:
854
with MFISINDb() as db:
955
return db.nav_lookup(isin)
56+
57+
58+
def get_fin_year(dt: date):
59+
"""Get financial year representation."""
60+
if dt.month > 3:
61+
year1, year2 = dt.year, dt.year + 1
62+
else:
63+
year1, year2 = dt.year - 1, dt.year
64+
65+
if year1 % 100 != 99:
66+
year2 %= 100
67+
68+
return f"FY{year1}-{year2}"

casparser/cli.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,30 +155,35 @@ def print_gains(data, output_file_path=None):
155155
table.add_column("LTCG")
156156
table.add_column("LTCG (Taxable)")
157157
table.add_column("STCG")
158+
table.add_column("STCG (Taxable)")
158159

159160
for fy, rows in itertools.groupby(summary, lambda x: x[0]):
160161
table.add_row(f"[bold]{fy}[/]", "", "", "")
161162
ltcg_total = Decimal(0.0)
162163
stcg_total = Decimal(0.0)
163164
ltcg_taxable_total = Decimal(0.0)
165+
stcg_taxable_total = Decimal(0.0)
164166
for row in rows:
165-
_, fund, _, _, ltcg, ltcg_taxable, stcg = row
167+
_, fund, _, _, ltcg, ltcg_taxable, stcg, stcg_taxable = row
166168
ltcg_total += ltcg
167169
stcg_total += stcg
168170
ltcg_taxable_total += ltcg_taxable
171+
stcg_taxable_total += stcg_taxable
169172
table.add_row(
170173
"",
171174
fund,
172175
f"₹{round(ltcg, 2)}",
173176
f"₹{round(ltcg_taxable, 2)}",
174177
f"₹{round(stcg, 2)}",
178+
f"₹{round(stcg_taxable, 2)}",
175179
)
176180
table.add_row(
177181
"",
178182
f"[bold]{fy} - Total Gains[/]",
179183
f"[bold {get_color(ltcg_total)}]₹{round(ltcg_total, 2)}[/]",
180184
f"[bold {get_color(ltcg_taxable_total)}]₹{round(ltcg_taxable_total, 2)}[/]",
181185
f"[bold {get_color(stcg_total)}]₹{round(stcg_total, 2)}[/]",
186+
f"[bold {get_color(stcg_taxable_total)}]₹{round(stcg_taxable_total, 2)}[/]",
182187
)
183188
console.print(table)
184189
if isinstance(output_file_path, str):

tests/test_gains.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from datetime import date
2+
3+
import pytest
4+
5+
6+
from casparser.analysis.utils import CII, get_fin_year
7+
8+
9+
class TestGainsClass:
10+
def test_cii(self):
11+
# Invalid FY
12+
with pytest.raises(ValueError):
13+
CII["2000-01"]
14+
with pytest.raises(KeyError):
15+
CII["FY2001-05"]
16+
17+
# Tests
18+
assert abs(CII["FY2020-21"] / CII["FY2001-02"] - 3.01) <= 1e-3
19+
20+
# Checks for out-of-range FYs
21+
today = date.today()
22+
future_date = date(today.year + 3, today.month, today.day)
23+
assert CII["FY1990-91"] == 100
24+
assert CII[get_fin_year(future_date)] == CII[CII._max_year]

0 commit comments

Comments
 (0)