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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.