fa-inrdata-api 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from .core import (
|
|
2
|
+
PriceRow,
|
|
3
|
+
ScheduleFASummary,
|
|
4
|
+
Ticker,
|
|
5
|
+
TickerInfo,
|
|
6
|
+
date_range,
|
|
7
|
+
max_value,
|
|
8
|
+
min_value,
|
|
9
|
+
price_action,
|
|
10
|
+
schedule_fa_summary,
|
|
11
|
+
supported_tickers,
|
|
12
|
+
supported_years_for_ticker,
|
|
13
|
+
ticker_metadata,
|
|
14
|
+
value_at_year_end,
|
|
15
|
+
value_at_year_start,
|
|
16
|
+
value_on_date,
|
|
17
|
+
warm_cache,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"PriceRow",
|
|
22
|
+
"ScheduleFASummary",
|
|
23
|
+
"Ticker",
|
|
24
|
+
"TickerInfo",
|
|
25
|
+
"date_range",
|
|
26
|
+
"max_value",
|
|
27
|
+
"min_value",
|
|
28
|
+
"price_action",
|
|
29
|
+
"schedule_fa_summary",
|
|
30
|
+
"supported_tickers",
|
|
31
|
+
"supported_years_for_ticker",
|
|
32
|
+
"ticker_metadata",
|
|
33
|
+
"value_at_year_end",
|
|
34
|
+
"value_at_year_start",
|
|
35
|
+
"value_on_date",
|
|
36
|
+
"warm_cache",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
__version__ = "0.1.0"
|
fa_inrdata_api/core.py
ADDED
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Read-only API over the jdecodes/fa-inrdata GitHub dataset.
|
|
3
|
+
|
|
4
|
+
Data source: https://github.com/jdecodes/fa-inrdata/tree/main/data
|
|
5
|
+
Each ticker has one CSV per completed calendar year at
|
|
6
|
+
data/{TICKER}/{YEAR}.csv
|
|
7
|
+
with columns: date, close, sbi_tt, close_inr
|
|
8
|
+
|
|
9
|
+
DISCLAIMER: This is an unofficial, community-maintained package, not
|
|
10
|
+
affiliated with SBI or Yahoo Finance. Data may be inaccurate or
|
|
11
|
+
incomplete. Verify against an official source before relying on this
|
|
12
|
+
for tax or financial decisions.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import csv
|
|
18
|
+
import io
|
|
19
|
+
import json
|
|
20
|
+
import re
|
|
21
|
+
import urllib.error
|
|
22
|
+
import urllib.request
|
|
23
|
+
import warnings
|
|
24
|
+
from bisect import bisect_right
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
from datetime import date, datetime
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
warnings.warn(
|
|
30
|
+
"fa_inrdata_api is an UNOFFICIAL, community-maintained package. "
|
|
31
|
+
"Data may be inaccurate or incomplete. Verify against an official "
|
|
32
|
+
"source before relying on this for tax or financial decisions.",
|
|
33
|
+
UserWarning,
|
|
34
|
+
stacklevel=2,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
RAW_BASE = "https://raw.githubusercontent.com/jdecodes/fa-inrdata/main"
|
|
38
|
+
TICKERS_URL = f"{RAW_BASE}/tickers.csv"
|
|
39
|
+
MANIFEST_URL = f"{RAW_BASE}/manifest.json"
|
|
40
|
+
DATA_URL_TEMPLATE = RAW_BASE + "/data/{ticker}/{year}.csv"
|
|
41
|
+
|
|
42
|
+
CACHE_DIR = Path.home() / ".cache" / "fa_inrdata_api"
|
|
43
|
+
|
|
44
|
+
_DATE_RE_FMT = "%Y-%m-%d"
|
|
45
|
+
_DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# --------------------------------------------------------------------------
|
|
49
|
+
# Data types
|
|
50
|
+
# --------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class TickerInfo:
|
|
55
|
+
ticker: str
|
|
56
|
+
name: str
|
|
57
|
+
address: str
|
|
58
|
+
zip: str
|
|
59
|
+
country: str
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True)
|
|
63
|
+
class PriceRow:
|
|
64
|
+
date: str # YYYY-MM-DD
|
|
65
|
+
close: float # native currency (e.g. USD) close
|
|
66
|
+
sbi_tt: float # SBI TT buying rate applied
|
|
67
|
+
close_inr: float # close converted to INR
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True)
|
|
71
|
+
class ScheduleFASummary:
|
|
72
|
+
ticker: str
|
|
73
|
+
year: int
|
|
74
|
+
initial: PriceRow # first trading day of the year
|
|
75
|
+
peak: PriceRow # highest close_inr in the year
|
|
76
|
+
closing: PriceRow # last trading day of the year
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# --------------------------------------------------------------------------
|
|
80
|
+
# Internal helpers
|
|
81
|
+
# --------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _http_get(url: str, timeout: int = 10) -> str:
|
|
85
|
+
try:
|
|
86
|
+
with urllib.request.urlopen(url, timeout=timeout) as resp:
|
|
87
|
+
return resp.read().decode()
|
|
88
|
+
except urllib.error.HTTPError as e:
|
|
89
|
+
raise ValueError(f"No data found at {url} (HTTP {e.code})") from e
|
|
90
|
+
except urllib.error.URLError as e:
|
|
91
|
+
raise ConnectionError(f"Could not reach {url}: {e.reason}") from e
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _cache_path(*parts: str) -> Path:
|
|
95
|
+
return CACHE_DIR.joinpath(*parts)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _read_cache(path: Path) -> str | None:
|
|
99
|
+
if path.exists():
|
|
100
|
+
return path.read_text()
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _write_cache(path: Path, text: str) -> None:
|
|
105
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
106
|
+
path.write_text(text)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _parse_date(value: str | date) -> date:
|
|
110
|
+
"""
|
|
111
|
+
Strict date validation, matching sbi_tt_rates' behavior.
|
|
112
|
+
|
|
113
|
+
Accepts:
|
|
114
|
+
- a real datetime.date (including datetime.datetime, which is a
|
|
115
|
+
date subclass) object — the preferred input
|
|
116
|
+
- a string strictly in 'YYYY-MM-DD' format, zero-padded
|
|
117
|
+
(e.g. '2025-01-07', not '2025-1-7')
|
|
118
|
+
|
|
119
|
+
Rejects everything else with a clear error, rather than letting a
|
|
120
|
+
permissive strptime silently accept ambiguous formats.
|
|
121
|
+
"""
|
|
122
|
+
if isinstance(value, date):
|
|
123
|
+
return value
|
|
124
|
+
|
|
125
|
+
if not isinstance(value, str):
|
|
126
|
+
raise TypeError(
|
|
127
|
+
f"Expected a datetime.date object or a 'YYYY-MM-DD' string, "
|
|
128
|
+
f"got {type(value).__name__}: {value!r}"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if not _DATE_RE.match(value):
|
|
132
|
+
raise ValueError(
|
|
133
|
+
f"Invalid date format: {value!r}. Expected zero-padded "
|
|
134
|
+
f"'YYYY-MM-DD' (e.g. '2025-01-07'), not e.g. '2025-1-7'."
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
return datetime.strptime(value, _DATE_RE_FMT).date()
|
|
139
|
+
except ValueError as e:
|
|
140
|
+
raise ValueError(f"Invalid date: {value!r} is not a real calendar date.") from e
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _load_manifest(refresh: bool = False) -> dict | None:
|
|
144
|
+
"""Fetch manifest.json (ticker -> [years]). Returns None if it 404s
|
|
145
|
+
(e.g. not yet published upstream) so callers can fall back gracefully."""
|
|
146
|
+
cache_path = _cache_path("manifest.json")
|
|
147
|
+
text = None if refresh else _read_cache(cache_path)
|
|
148
|
+
if text is None:
|
|
149
|
+
try:
|
|
150
|
+
text = _http_get(MANIFEST_URL)
|
|
151
|
+
except (ValueError, ConnectionError):
|
|
152
|
+
return None
|
|
153
|
+
_write_cache(cache_path, text)
|
|
154
|
+
try:
|
|
155
|
+
return json.loads(text)
|
|
156
|
+
except json.JSONDecodeError:
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _load_ticker_rows(refresh: bool = False) -> list[dict]:
|
|
161
|
+
cache_path = _cache_path("tickers.csv")
|
|
162
|
+
text = None if refresh else _read_cache(cache_path)
|
|
163
|
+
if text is None:
|
|
164
|
+
text = _http_get(TICKERS_URL)
|
|
165
|
+
_write_cache(cache_path, text)
|
|
166
|
+
return list(csv.DictReader(io.StringIO(text)))
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _load_price_rows(ticker: str, year: int, refresh: bool = False) -> list[PriceRow]:
|
|
170
|
+
ticker = ticker.upper()
|
|
171
|
+
cache_path = _cache_path(ticker, f"{year}.csv")
|
|
172
|
+
text = None if refresh else _read_cache(cache_path)
|
|
173
|
+
if text is None:
|
|
174
|
+
url = DATA_URL_TEMPLATE.format(ticker=ticker, year=year)
|
|
175
|
+
try:
|
|
176
|
+
text = _http_get(url)
|
|
177
|
+
except ValueError as e:
|
|
178
|
+
raise ValueError(
|
|
179
|
+
f"No data for ticker={ticker!r}, year={year}. "
|
|
180
|
+
f"Call supported_years_for_ticker({ticker!r}) to see what's "
|
|
181
|
+
f"available."
|
|
182
|
+
) from e
|
|
183
|
+
_write_cache(cache_path, text)
|
|
184
|
+
|
|
185
|
+
rows = []
|
|
186
|
+
for r in csv.DictReader(io.StringIO(text)):
|
|
187
|
+
rows.append(
|
|
188
|
+
PriceRow(
|
|
189
|
+
date=r["date"],
|
|
190
|
+
close=float(r["close"]),
|
|
191
|
+
sbi_tt=float(r["sbi_tt"]),
|
|
192
|
+
close_inr=float(r["close_inr"]),
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
rows.sort(key=lambda row: row.date)
|
|
196
|
+
return rows
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# --------------------------------------------------------------------------
|
|
200
|
+
# Public API
|
|
201
|
+
# --------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def warm_cache(refresh: bool = False) -> None:
|
|
205
|
+
"""
|
|
206
|
+
Eagerly fetch and cache tickers.csv + manifest.json.
|
|
207
|
+
|
|
208
|
+
Not required — every function fetches+caches lazily on first call
|
|
209
|
+
anyway — but calling this once (e.g. the first cell of a notebook,
|
|
210
|
+
or right after pip install) avoids the first real call paying for
|
|
211
|
+
both downloads, and surfaces network/auth issues up front instead
|
|
212
|
+
of mid-analysis.
|
|
213
|
+
"""
|
|
214
|
+
_load_ticker_rows(refresh=refresh)
|
|
215
|
+
_load_manifest(refresh=refresh)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def supported_tickers(refresh: bool = False) -> list[str]:
|
|
219
|
+
"""Return the sorted list of tickers this dataset covers."""
|
|
220
|
+
rows = _load_ticker_rows(refresh=refresh)
|
|
221
|
+
return sorted(r["ticker"] for r in rows)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def ticker_metadata(ticker: str, refresh: bool = False) -> TickerInfo:
|
|
225
|
+
"""Return name/address/zip/country for a ticker (from tickers.csv)."""
|
|
226
|
+
ticker = ticker.upper()
|
|
227
|
+
rows = _load_ticker_rows(refresh=refresh)
|
|
228
|
+
for r in rows:
|
|
229
|
+
if r["ticker"].upper() == ticker:
|
|
230
|
+
return TickerInfo(
|
|
231
|
+
ticker=r["ticker"],
|
|
232
|
+
name=r["name"],
|
|
233
|
+
address=r["address"],
|
|
234
|
+
zip=r["zip"],
|
|
235
|
+
country=r["country"],
|
|
236
|
+
)
|
|
237
|
+
raise ValueError(f"Unsupported ticker: {ticker!r}. Call supported_tickers() for the full list.")
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def supported_years_for_ticker(ticker: str, refresh: bool = False) -> list[int]:
|
|
241
|
+
"""
|
|
242
|
+
Return years for which ticker/{year}.csv exists, per manifest.json
|
|
243
|
+
(the single source of truth — no probing, no guessing).
|
|
244
|
+
"""
|
|
245
|
+
ticker = ticker.upper()
|
|
246
|
+
manifest = _load_manifest(refresh=refresh)
|
|
247
|
+
if manifest is None:
|
|
248
|
+
raise RuntimeError(
|
|
249
|
+
f"manifest.json is unavailable at {MANIFEST_URL} "
|
|
250
|
+
f"(network issue, or not published upstream)."
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
years = manifest.get("tickers", {}).get(ticker)
|
|
254
|
+
if years is None:
|
|
255
|
+
raise ValueError(
|
|
256
|
+
f"No manifest entry for ticker={ticker!r}. Call "
|
|
257
|
+
f"supported_tickers() for the full list, or the ticker may "
|
|
258
|
+
f"not have any generated data yet."
|
|
259
|
+
)
|
|
260
|
+
return sorted(years)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def price_action(ticker: str, year: int, refresh: bool = False) -> list[PriceRow]:
|
|
264
|
+
"""Full year of (date, close, sbi_tt, close_inr) rows, sorted by date."""
|
|
265
|
+
return _load_price_rows(ticker, year, refresh=refresh)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def date_range(ticker: str, year: int) -> tuple[str, str]:
|
|
269
|
+
"""First and last trading-day dates present for ticker/year."""
|
|
270
|
+
rows = price_action(ticker, year)
|
|
271
|
+
if not rows:
|
|
272
|
+
raise ValueError(f"No rows for ticker={ticker!r}, year={year}")
|
|
273
|
+
return rows[0].date, rows[-1].date
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def value_at_year_start(ticker: str, year: int) -> PriceRow:
|
|
277
|
+
"""First trading day's row for the year (initial investment value)."""
|
|
278
|
+
rows = price_action(ticker, year)
|
|
279
|
+
if not rows:
|
|
280
|
+
raise ValueError(f"No rows for ticker={ticker!r}, year={year}")
|
|
281
|
+
return rows[0]
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def value_at_year_end(ticker: str, year: int) -> PriceRow:
|
|
285
|
+
"""Last trading day's row for the year (closing balance value)."""
|
|
286
|
+
rows = price_action(ticker, year)
|
|
287
|
+
if not rows:
|
|
288
|
+
raise ValueError(f"No rows for ticker={ticker!r}, year={year}")
|
|
289
|
+
return rows[-1]
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def value_on_date(ticker: str, target_date: str | date) -> PriceRow:
|
|
293
|
+
"""
|
|
294
|
+
Row for target_date, falling back to the nearest prior trading day
|
|
295
|
+
if the exact date isn't a trading day (weekends/holidays).
|
|
296
|
+
|
|
297
|
+
target_date: prefer a datetime.date object; a strict zero-padded
|
|
298
|
+
'YYYY-MM-DD' string is also accepted.
|
|
299
|
+
"""
|
|
300
|
+
d = _parse_date(target_date)
|
|
301
|
+
rows = price_action(ticker, d.year)
|
|
302
|
+
dates = [r.date for r in rows]
|
|
303
|
+
target_str = d.isoformat()
|
|
304
|
+
|
|
305
|
+
idx = bisect_right(dates, target_str) - 1
|
|
306
|
+
if idx < 0:
|
|
307
|
+
raise ValueError(
|
|
308
|
+
f"No trading day on or before {target_str} in ticker={ticker!r} year={d.year} data."
|
|
309
|
+
)
|
|
310
|
+
return rows[idx]
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def max_value(
|
|
314
|
+
ticker: str,
|
|
315
|
+
year: int,
|
|
316
|
+
start_date: str | date | None = None,
|
|
317
|
+
end_date: str | date | None = None,
|
|
318
|
+
) -> PriceRow:
|
|
319
|
+
"""
|
|
320
|
+
Peak close_inr row for the year, or within [start_date, end_date] if
|
|
321
|
+
given (both inclusive; either may be omitted to leave that side open).
|
|
322
|
+
|
|
323
|
+
start_date/end_date: prefer datetime.date objects; strict zero-padded
|
|
324
|
+
'YYYY-MM-DD' strings are also accepted.
|
|
325
|
+
"""
|
|
326
|
+
rows = price_action(ticker, year)
|
|
327
|
+
rows = _window(rows, start_date, end_date)
|
|
328
|
+
if not rows:
|
|
329
|
+
raise ValueError(f"No rows in requested window for {ticker!r}/{year}")
|
|
330
|
+
return max(rows, key=lambda r: r.close_inr)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def min_value(
|
|
334
|
+
ticker: str,
|
|
335
|
+
year: int,
|
|
336
|
+
start_date: str | date | None = None,
|
|
337
|
+
end_date: str | date | None = None,
|
|
338
|
+
) -> PriceRow:
|
|
339
|
+
"""Lowest close_inr row for the year, or within a window. See max_value."""
|
|
340
|
+
rows = price_action(ticker, year)
|
|
341
|
+
rows = _window(rows, start_date, end_date)
|
|
342
|
+
if not rows:
|
|
343
|
+
raise ValueError(f"No rows in requested window for {ticker!r}/{year}")
|
|
344
|
+
return min(rows, key=lambda r: r.close_inr)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _window(
|
|
348
|
+
rows: list[PriceRow],
|
|
349
|
+
start_date: str | date | None,
|
|
350
|
+
end_date: str | date | None,
|
|
351
|
+
) -> list[PriceRow]:
|
|
352
|
+
if start_date is None and end_date is None:
|
|
353
|
+
return rows
|
|
354
|
+
lo = _parse_date(start_date).isoformat() if start_date else None
|
|
355
|
+
hi = _parse_date(end_date).isoformat() if end_date else None
|
|
356
|
+
return [r for r in rows if (lo is None or r.date >= lo) and (hi is None or r.date <= hi)]
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def schedule_fa_summary(ticker: str, year: int) -> ScheduleFASummary:
|
|
360
|
+
"""
|
|
361
|
+
One-shot convenience call for the values Schedule FA actually needs:
|
|
362
|
+
initial value, peak value, and closing value for the year.
|
|
363
|
+
"""
|
|
364
|
+
rows = price_action(ticker, year)
|
|
365
|
+
if not rows:
|
|
366
|
+
raise ValueError(f"No rows for ticker={ticker!r}, year={year}")
|
|
367
|
+
return ScheduleFASummary(
|
|
368
|
+
ticker=ticker.upper(),
|
|
369
|
+
year=year,
|
|
370
|
+
initial=rows[0],
|
|
371
|
+
peak=max(rows, key=lambda r: r.close_inr),
|
|
372
|
+
closing=rows[-1],
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
# --------------------------------------------------------------------------
|
|
377
|
+
# Object-oriented convenience wrapper
|
|
378
|
+
# --------------------------------------------------------------------------
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
class Ticker:
|
|
382
|
+
"""
|
|
383
|
+
Thin object wrapper so repeated (ticker, year) pairs don't have to be
|
|
384
|
+
passed to every call. Every method here just delegates to the plain
|
|
385
|
+
functions above — no separate logic, so behavior is identical either
|
|
386
|
+
way. Prefer this when working with one ticker+year across several
|
|
387
|
+
calls; prefer the plain functions when looping over many tickers/years,
|
|
388
|
+
since the object buys nothing there.
|
|
389
|
+
|
|
390
|
+
>>> t = Ticker("AAPL", 2025)
|
|
391
|
+
>>> t.schedule_fa_summary()
|
|
392
|
+
>>> t.max_value()
|
|
393
|
+
|
|
394
|
+
>>> t2 = Ticker("AAPL") # year not known yet
|
|
395
|
+
>>> t2.supported_years()
|
|
396
|
+
>>> t2.for_year(2025).schedule_fa_summary()
|
|
397
|
+
"""
|
|
398
|
+
|
|
399
|
+
def __init__(self, symbol: str, year: int | None = None):
|
|
400
|
+
self.symbol = symbol.upper()
|
|
401
|
+
self.year = year
|
|
402
|
+
|
|
403
|
+
def __repr__(self) -> str:
|
|
404
|
+
return f"Ticker({self.symbol!r}, year={self.year!r})"
|
|
405
|
+
|
|
406
|
+
def for_year(self, year: int) -> "Ticker":
|
|
407
|
+
"""Return a copy of this Ticker scoped to a different year."""
|
|
408
|
+
return Ticker(self.symbol, year)
|
|
409
|
+
|
|
410
|
+
def _require_year(self) -> int:
|
|
411
|
+
if self.year is None:
|
|
412
|
+
raise ValueError(
|
|
413
|
+
f"{self!r} has no year set. Pass year=... to Ticker(...) "
|
|
414
|
+
f"or call .for_year(year) first."
|
|
415
|
+
)
|
|
416
|
+
return self.year
|
|
417
|
+
|
|
418
|
+
# -- ticker-level, no year required --
|
|
419
|
+
|
|
420
|
+
def metadata(self, refresh: bool = False) -> TickerInfo:
|
|
421
|
+
return ticker_metadata(self.symbol, refresh=refresh)
|
|
422
|
+
|
|
423
|
+
def supported_years(self, refresh: bool = False) -> list[int]:
|
|
424
|
+
return supported_years_for_ticker(self.symbol, refresh=refresh)
|
|
425
|
+
|
|
426
|
+
def value_on(self, target_date: str | date) -> PriceRow:
|
|
427
|
+
"""Doesn't need self.year — the year is derived from target_date."""
|
|
428
|
+
return value_on_date(self.symbol, target_date)
|
|
429
|
+
|
|
430
|
+
# -- year-scoped, require self.year --
|
|
431
|
+
|
|
432
|
+
def price_action(self, refresh: bool = False) -> list[PriceRow]:
|
|
433
|
+
return price_action(self.symbol, self._require_year(), refresh=refresh)
|
|
434
|
+
|
|
435
|
+
def date_range(self) -> tuple[str, str]:
|
|
436
|
+
return date_range(self.symbol, self._require_year())
|
|
437
|
+
|
|
438
|
+
def value_at_start(self) -> PriceRow:
|
|
439
|
+
return value_at_year_start(self.symbol, self._require_year())
|
|
440
|
+
|
|
441
|
+
def value_at_end(self) -> PriceRow:
|
|
442
|
+
return value_at_year_end(self.symbol, self._require_year())
|
|
443
|
+
|
|
444
|
+
def max_value(
|
|
445
|
+
self,
|
|
446
|
+
start_date: str | date | None = None,
|
|
447
|
+
end_date: str | date | None = None,
|
|
448
|
+
) -> PriceRow:
|
|
449
|
+
return max_value(self.symbol, self._require_year(), start_date, end_date)
|
|
450
|
+
|
|
451
|
+
def min_value(
|
|
452
|
+
self,
|
|
453
|
+
start_date: str | date | None = None,
|
|
454
|
+
end_date: str | date | None = None,
|
|
455
|
+
) -> PriceRow:
|
|
456
|
+
return min_value(self.symbol, self._require_year(), start_date, end_date)
|
|
457
|
+
|
|
458
|
+
def schedule_fa_summary(self) -> ScheduleFASummary:
|
|
459
|
+
return schedule_fa_summary(self.symbol, self._require_year())
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fa-inrdata-api
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python API for historical INR valuations of US-listed stocks, designed for Indian Income Tax Schedule FA reporting.
|
|
5
|
+
Project-URL: Homepage, https://github.com/jdecodes/fa-inrdata
|
|
6
|
+
Project-URL: Repository, https://github.com/jdecodes/fa-inrdata
|
|
7
|
+
Project-URL: Issues, https://github.com/jdecodes/fa-inrdata/issues
|
|
8
|
+
Author-email: jdecodes <jaideep_sharma@live.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: exchange-rate,finance,foreign-assets,forex,income-tax,india,itr,sbi,schedule-fa,stocks
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# fa-inrdata-api
|
|
26
|
+
|
|
27
|
+
> A lightweight Python API for historical INR-adjusted stock prices, built specifically for Indian Income Tax **Schedule FA** reporting.
|
|
28
|
+
|
|
29
|
+
`fa-inrdata-api` provides historical daily stock prices already converted to INR using the corresponding **State Bank of India (SBI) TT Buying Rate** for that date. Instead of downloading Yahoo Finance prices, searching SBI TT rates, and performing currency conversion yourself, this package gives you the final INR value in one API call.
|
|
30
|
+
|
|
31
|
+
The data is powered by the **fa-inrdata** dataset.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Why?
|
|
36
|
+
|
|
37
|
+
While filing **Schedule FA (Foreign Assets)** in the Indian Income Tax Return, taxpayers are required to report values such as:
|
|
38
|
+
|
|
39
|
+
- Initial value of investment
|
|
40
|
+
- Peak value during the year
|
|
41
|
+
- Closing balance
|
|
42
|
+
- Value on a specific acquisition date
|
|
43
|
+
|
|
44
|
+
Obtaining these values is surprisingly tedious.
|
|
45
|
+
|
|
46
|
+
Typically you need to
|
|
47
|
+
|
|
48
|
+
- download historical stock prices
|
|
49
|
+
- obtain historical SBI TT buying rates
|
|
50
|
+
- convert every day's closing price into INR
|
|
51
|
+
- determine the maximum INR value for the required period
|
|
52
|
+
|
|
53
|
+
This package performs those steps for you.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Features
|
|
58
|
+
|
|
59
|
+
- Historical daily close prices
|
|
60
|
+
- Historical SBI TT Buying rates
|
|
61
|
+
- Daily INR converted prices
|
|
62
|
+
- Initial value for a year
|
|
63
|
+
- Closing value for a year
|
|
64
|
+
- Peak INR value for a year
|
|
65
|
+
- Peak INR value within a custom date range
|
|
66
|
+
- Value on any date (with automatic previous trading day fallback)
|
|
67
|
+
- Ticker metadata
|
|
68
|
+
- Local caching
|
|
69
|
+
- Zero third-party dependencies
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Installation
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
pip install fa-inrdata-api
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Requires Python **3.10+**
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Quick Example
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
import fa_inrdata_api as fa
|
|
87
|
+
|
|
88
|
+
summary = fa.schedule_fa_summary("AAPL", 2025)
|
|
89
|
+
|
|
90
|
+
print(summary.initial)
|
|
91
|
+
print(summary.peak)
|
|
92
|
+
print(summary.closing)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Using the Object-Oriented API
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from fa_inrdata_api import Ticker
|
|
101
|
+
|
|
102
|
+
aapl = Ticker("AAPL", 2025)
|
|
103
|
+
|
|
104
|
+
print(aapl.schedule_fa_summary())
|
|
105
|
+
print(aapl.max_value())
|
|
106
|
+
print(aapl.value_at_start())
|
|
107
|
+
print(aapl.value_at_end())
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Supported Tickers
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
import fa_inrdata_api as fa
|
|
116
|
+
|
|
117
|
+
print(fa.supported_tickers())
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Supported Years
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
import fa_inrdata_api as fa
|
|
126
|
+
|
|
127
|
+
print(fa.supported_years_for_ticker("AAPL"))
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Example
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
[2025]
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Get Company Metadata
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
import fa_inrdata_api as fa
|
|
142
|
+
|
|
143
|
+
info = fa.ticker_metadata("AAPL")
|
|
144
|
+
|
|
145
|
+
print(info)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Returns
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
TickerInfo(
|
|
152
|
+
ticker="AAPL",
|
|
153
|
+
name="Apple Inc.",
|
|
154
|
+
address="One Apple Park Way",
|
|
155
|
+
zip="95014",
|
|
156
|
+
country="United States",
|
|
157
|
+
)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Complete Daily Price History
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
rows = fa.price_action("AAPL", 2025)
|
|
166
|
+
|
|
167
|
+
print(rows[0])
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Returns
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
PriceRow(date="2025-01-02", close=243.85, sbi_tt=85.64, close_inr=20880.11)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Value on a Specific Date
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
import fa_inrdata_api as fa
|
|
182
|
+
|
|
183
|
+
row = fa.value_on_date("AAPL", "2025-05-17")
|
|
184
|
+
|
|
185
|
+
print(row)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
If the supplied date falls on a weekend or market holiday, the package automatically returns the nearest previous trading day.
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Initial Value
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
fa.value_at_year_start("AAPL", 2025)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Closing Value
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
fa.value_at_year_end("AAPL", 2025)
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## Maximum Value
|
|
209
|
+
|
|
210
|
+
Entire year
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
fa.max_value("AAPL", 2025)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Custom period
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
fa.max_value(
|
|
220
|
+
"AAPL",
|
|
221
|
+
2025,
|
|
222
|
+
start_date="2025-03-15",
|
|
223
|
+
end_date="2025-08-31",
|
|
224
|
+
)
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Minimum Value
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
fa.min_value("AAPL", 2025)
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## Schedule FA Summary
|
|
238
|
+
|
|
239
|
+
This convenience function returns everything typically required for Schedule FA.
|
|
240
|
+
|
|
241
|
+
```python
|
|
242
|
+
summary = fa.schedule_fa_summary("AAPL", 2025)
|
|
243
|
+
|
|
244
|
+
print(summary.initial)
|
|
245
|
+
print(summary.peak)
|
|
246
|
+
print(summary.closing)
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Returns
|
|
250
|
+
|
|
251
|
+
```python
|
|
252
|
+
ScheduleFASummary(ticker="AAPL", year=2025, initial=..., peak=..., closing=...)
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## Warm the Cache
|
|
258
|
+
|
|
259
|
+
The package downloads data lazily and stores it locally.
|
|
260
|
+
|
|
261
|
+
If desired, you can warm the cache beforehand.
|
|
262
|
+
|
|
263
|
+
```python
|
|
264
|
+
import fa_inrdata_api as fa
|
|
265
|
+
|
|
266
|
+
fa.warm_cache()
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## Caching
|
|
272
|
+
|
|
273
|
+
Downloaded files are cached locally under
|
|
274
|
+
|
|
275
|
+
```
|
|
276
|
+
~/.cache/fa_inrdata_api/
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Subsequent requests are served directly from the cache.
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## Data Source
|
|
284
|
+
|
|
285
|
+
This package reads data from
|
|
286
|
+
|
|
287
|
+
https://github.com/jdecodes/fa-inrdata
|
|
288
|
+
|
|
289
|
+
Each ticker has one CSV per calendar year.
|
|
290
|
+
|
|
291
|
+
```
|
|
292
|
+
data/
|
|
293
|
+
AAPL/
|
|
294
|
+
2023.csv
|
|
295
|
+
2024.csv
|
|
296
|
+
2025.csv
|
|
297
|
+
|
|
298
|
+
MSFT/
|
|
299
|
+
...
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Each CSV contains
|
|
303
|
+
|
|
304
|
+
| Column | Description |
|
|
305
|
+
|---------|-------------|
|
|
306
|
+
| date | Trading date |
|
|
307
|
+
| close | Yahoo Finance closing price |
|
|
308
|
+
| sbi_tt | SBI TT Buying Rate |
|
|
309
|
+
| close_inr | close × sbi_tt |
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## Why SBI TT Buying Rate?
|
|
314
|
+
|
|
315
|
+
Indian Income Tax guidance for Schedule FA generally requires foreign asset values to be converted into INR using the **State Bank of India Telegraphic Transfer (TT) Buying Rate**.
|
|
316
|
+
|
|
317
|
+
This package performs that conversion in advance.
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
## Disclaimer
|
|
322
|
+
|
|
323
|
+
This is an **unofficial**, community-maintained package.
|
|
324
|
+
|
|
325
|
+
It is **not affiliated with**
|
|
326
|
+
|
|
327
|
+
- State Bank of India (SBI)
|
|
328
|
+
- Yahoo Finance
|
|
329
|
+
- Indian Income Tax Department
|
|
330
|
+
|
|
331
|
+
Although every effort is made to ensure accuracy, the data may contain errors or omissions.
|
|
332
|
+
|
|
333
|
+
Always verify important values against official sources before filing taxes or making financial decisions.
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
## License
|
|
338
|
+
|
|
339
|
+
MIT License
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
## Related Project
|
|
344
|
+
|
|
345
|
+
This package is powered by the underlying dataset:
|
|
346
|
+
|
|
347
|
+
**fa-inrdata**
|
|
348
|
+
|
|
349
|
+
https://github.com/jdecodes/fa-inrdata
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
## Author
|
|
354
|
+
|
|
355
|
+
Built by **jdecodes** : https://github.com/jdecodes
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
Contributions, bug reports, feature requests, and pull requests are always welcome.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
fa_inrdata_api/__init__.py,sha256=Q_UML0EBfSUcSF8eF4Q22XCkSi5t-eoGqXVZuAeVWPo,757
|
|
2
|
+
fa_inrdata_api/core.py,sha256=TcluTVkYJaHBO2EoMvqdFc1CzI4FUZCRAOr1vVy6Jeo,15771
|
|
3
|
+
fa_inrdata_api-0.1.0.dist-info/METADATA,sha256=hie1xtE8bGLBJzVL1937tENMfsE7EKc9sPX1SMXzE6Q,6636
|
|
4
|
+
fa_inrdata_api-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
5
|
+
fa_inrdata_api-0.1.0.dist-info/licenses/LICENSE,sha256=-K0hxBCgXzTV9VIYz5avRkdRDzQ9G03FqaadbYI3UJU,1084
|
|
6
|
+
fa_inrdata_api-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 jdecodes
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|