bootlick 0.2.2__tar.gz → 0.2.3__tar.gz
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.
- {bootlick-0.2.2 → bootlick-0.2.3}/PKG-INFO +1 -1
- {bootlick-0.2.2 → bootlick-0.2.3}/bootlick/nse_eod_prices.py +148 -62
- {bootlick-0.2.2 → bootlick-0.2.3}/bootlick.egg-info/PKG-INFO +1 -1
- {bootlick-0.2.2 → bootlick-0.2.3}/pyproject.toml +1 -1
- {bootlick-0.2.2 → bootlick-0.2.3}/bootlick/__init__.py +0 -0
- {bootlick-0.2.2 → bootlick-0.2.3}/bootlick/bse_eod_prices.py +0 -0
- {bootlick-0.2.2 → bootlick-0.2.3}/bootlick/config/config_sample.yaml +0 -0
- {bootlick-0.2.2 → bootlick-0.2.3}/bootlick/config.py +0 -0
- {bootlick-0.2.2 → bootlick-0.2.3}/bootlick/nse_company_info.py +0 -0
- {bootlick-0.2.2 → bootlick-0.2.3}/bootlick/screener.py +0 -0
- {bootlick-0.2.2 → bootlick-0.2.3}/bootlick/screener_metrics.py +0 -0
- {bootlick-0.2.2 → bootlick-0.2.3}/bootlick/testing.py +0 -0
- {bootlick-0.2.2 → bootlick-0.2.3}/bootlick.egg-info/SOURCES.txt +0 -0
- {bootlick-0.2.2 → bootlick-0.2.3}/bootlick.egg-info/dependency_links.txt +0 -0
- {bootlick-0.2.2 → bootlick-0.2.3}/bootlick.egg-info/requires.txt +0 -0
- {bootlick-0.2.2 → bootlick-0.2.3}/bootlick.egg-info/top_level.txt +0 -0
- {bootlick-0.2.2 → bootlick-0.2.3}/setup.cfg +0 -0
|
@@ -62,6 +62,149 @@ def get_dynamic_config():
|
|
|
62
62
|
return get_config()
|
|
63
63
|
|
|
64
64
|
|
|
65
|
+
# Canonical columns written to {date}_mf.csv. Process mode and historical files depend on these names.
|
|
66
|
+
_AMFI_NAV_OUTPUT_COLUMNS = [
|
|
67
|
+
"code",
|
|
68
|
+
"scheme",
|
|
69
|
+
"isin_growth_dividend",
|
|
70
|
+
"isin_reinvestment",
|
|
71
|
+
"nav",
|
|
72
|
+
"repurchase_price",
|
|
73
|
+
"sale_price",
|
|
74
|
+
"date",
|
|
75
|
+
"category",
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _amfi_colmap(columns):
|
|
80
|
+
return {str(c).strip().lower(): c for c in columns}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _amfi_resolve_col(colmap, *aliases):
|
|
84
|
+
for alias in aliases:
|
|
85
|
+
if alias in colmap:
|
|
86
|
+
return colmap[alias]
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _is_amfi_isin(value):
|
|
91
|
+
"""True for AMFI scheme ISINs (INF...), not Plan/Option text like 'INSTITUTIONAL...'."""
|
|
92
|
+
if not pd.notna(value):
|
|
93
|
+
return False
|
|
94
|
+
text = str(value).strip()
|
|
95
|
+
return len(text) >= 12 and text.startswith("IN") and " " not in text
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _join_amfi_scheme(name, plan, option):
|
|
99
|
+
parts = []
|
|
100
|
+
for value in (name, plan, option):
|
|
101
|
+
if pd.notna(value):
|
|
102
|
+
text = str(value).strip()
|
|
103
|
+
if text:
|
|
104
|
+
parts.append(text)
|
|
105
|
+
return " - ".join(parts) if parts else pd.NA
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _canonicalize_amfi_nav_frame(data: pd.DataFrame) -> pd.DataFrame:
|
|
109
|
+
"""Map old (Repurchase/Sale) and new (Plan/Option) AMFI headers to one schema."""
|
|
110
|
+
colmap = _amfi_colmap(data.columns)
|
|
111
|
+
code_col = _amfi_resolve_col(colmap, "scheme code", "code")
|
|
112
|
+
name_col = _amfi_resolve_col(colmap, "scheme name", "nav name", "scheme")
|
|
113
|
+
isin_growth_col = _amfi_resolve_col(
|
|
114
|
+
colmap, "isin div payout/isin growth", "isin growth", "isin div payout"
|
|
115
|
+
)
|
|
116
|
+
isin_reinv_col = _amfi_resolve_col(colmap, "isin div reinvestment")
|
|
117
|
+
nav_col = _amfi_resolve_col(colmap, "net asset value", "nav")
|
|
118
|
+
date_col = _amfi_resolve_col(colmap, "date")
|
|
119
|
+
required = {
|
|
120
|
+
"scheme code": code_col,
|
|
121
|
+
"scheme name": name_col,
|
|
122
|
+
"isin div payout/isin growth": isin_growth_col,
|
|
123
|
+
"isin div reinvestment": isin_reinv_col,
|
|
124
|
+
"net asset value": nav_col,
|
|
125
|
+
"date": date_col,
|
|
126
|
+
}
|
|
127
|
+
missing = [name for name, col in required.items() if col is None]
|
|
128
|
+
if missing:
|
|
129
|
+
raise ValueError(
|
|
130
|
+
f"Unrecognized AMFI NAV header, missing: {missing}; got: {[str(c).strip() for c in data.columns]}"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
scheme_name = data[name_col]
|
|
134
|
+
plan_col = _amfi_resolve_col(colmap, "plan")
|
|
135
|
+
option_col = _amfi_resolve_col(colmap, "option")
|
|
136
|
+
if plan_col is not None or option_col is not None:
|
|
137
|
+
plan = data[plan_col] if plan_col is not None else pd.Series(pd.NA, index=data.index)
|
|
138
|
+
option = data[option_col] if option_col is not None else pd.Series(pd.NA, index=data.index)
|
|
139
|
+
scheme = [_join_amfi_scheme(n, p, o) for n, p, o in zip(scheme_name, plan, option)]
|
|
140
|
+
else:
|
|
141
|
+
scheme = scheme_name
|
|
142
|
+
|
|
143
|
+
repurchase_col = _amfi_resolve_col(colmap, "repurchase price")
|
|
144
|
+
sale_col = _amfi_resolve_col(colmap, "sale price")
|
|
145
|
+
repurchase = data[repurchase_col] if repurchase_col is not None else pd.Series(pd.NA, index=data.index)
|
|
146
|
+
sale = data[sale_col] if sale_col is not None else pd.Series(pd.NA, index=data.index)
|
|
147
|
+
category = data["category"] if "category" in data.columns else pd.Series(pd.NA, index=data.index)
|
|
148
|
+
|
|
149
|
+
return pd.DataFrame(
|
|
150
|
+
{
|
|
151
|
+
"code": data[code_col],
|
|
152
|
+
"scheme": scheme,
|
|
153
|
+
"isin_growth_dividend": data[isin_growth_col],
|
|
154
|
+
"isin_reinvestment": data[isin_reinv_col],
|
|
155
|
+
"nav": data[nav_col],
|
|
156
|
+
"repurchase_price": repurchase,
|
|
157
|
+
"sale_price": sale,
|
|
158
|
+
"date": data[date_col],
|
|
159
|
+
"category": category,
|
|
160
|
+
}
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _amfi_process_category(value):
|
|
165
|
+
try:
|
|
166
|
+
if value == "" or not np.isnan(pd.to_numeric(value, errors="coerce")):
|
|
167
|
+
return np.nan
|
|
168
|
+
except ValueError:
|
|
169
|
+
pass
|
|
170
|
+
return value
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def parse_amfi_nav_csv(content: str) -> pd.DataFrame:
|
|
174
|
+
"""Parse AMFI NAV history text (legacy or Plan/Option header) into the saved CSV schema."""
|
|
175
|
+
data = pd.read_csv(
|
|
176
|
+
io.StringIO(content),
|
|
177
|
+
sep=";",
|
|
178
|
+
engine="python",
|
|
179
|
+
na_values=["", " "],
|
|
180
|
+
dtype=str,
|
|
181
|
+
skip_blank_lines=False,
|
|
182
|
+
on_bad_lines="warn",
|
|
183
|
+
)
|
|
184
|
+
data.columns = [str(c).strip() for c in data.columns]
|
|
185
|
+
data = data.dropna(how="all")
|
|
186
|
+
rows_to_remove = (data.isna().sum(axis=1) == len(data.columns) - 1) & data.iloc[:, 0].str.contains(
|
|
187
|
+
"Mutual Fund", na=False
|
|
188
|
+
)
|
|
189
|
+
data = data[~rows_to_remove]
|
|
190
|
+
data.loc[:, "category"] = data.iloc[:, 0]
|
|
191
|
+
data["category"] = data["category"].apply(_amfi_process_category)
|
|
192
|
+
data["category"] = data["category"].ffill()
|
|
193
|
+
data = data[data.isna().sum(axis=1) < len(data.columns) - 2]
|
|
194
|
+
data = _canonicalize_amfi_nav_frame(data)
|
|
195
|
+
for col in data.columns:
|
|
196
|
+
data[col] = data[col].apply(lambda x: x.strip() if isinstance(x, str) else x)
|
|
197
|
+
data["date"] = pd.to_datetime(data["date"], format="%d-%b-%Y")
|
|
198
|
+
data["isin_growth_dividend"] = data["isin_growth_dividend"].apply(
|
|
199
|
+
lambda x: x if _is_amfi_isin(x) else pd.NA
|
|
200
|
+
)
|
|
201
|
+
data["isin_reinvestment"] = data["isin_reinvestment"].apply(
|
|
202
|
+
lambda x: x if _is_amfi_isin(x) else pd.NA
|
|
203
|
+
)
|
|
204
|
+
data = data[data["isin_growth_dividend"].notna() | data["isin_reinvestment"].notna()]
|
|
205
|
+
return data[_AMFI_NAV_OUTPUT_COLUMNS]
|
|
206
|
+
|
|
207
|
+
|
|
65
208
|
def catch(func, *args, handle=lambda e: e, **kwargs):
|
|
66
209
|
try:
|
|
67
210
|
return func(*args, **kwargs)
|
|
@@ -788,57 +931,7 @@ def save_mf_data(p: str, session, save_to_rds=True):
|
|
|
788
931
|
{"url": url, "preview": first_line[:200]},
|
|
789
932
|
)
|
|
790
933
|
return None
|
|
791
|
-
|
|
792
|
-
# Use the Python engine so pandas can handle rows with variable field counts.
|
|
793
|
-
data = pd.read_csv(
|
|
794
|
-
io.StringIO(content),
|
|
795
|
-
sep=";",
|
|
796
|
-
engine="python",
|
|
797
|
-
na_values=["", " "],
|
|
798
|
-
dtype=str,
|
|
799
|
-
skip_blank_lines=False,
|
|
800
|
-
)
|
|
801
|
-
data = data.dropna(how="all")
|
|
802
|
-
rows_to_remove = (data.isna().sum(axis=1) == len(data.columns) - 1) & data.iloc[:, 0].str.contains(
|
|
803
|
-
"Mutual Fund", na=False
|
|
804
|
-
)
|
|
805
|
-
data = data[~rows_to_remove]
|
|
806
|
-
# Transpose category to each row
|
|
807
|
-
data.loc[:, "category"] = data.iloc[:, 0] # Assuming the category is in the first column
|
|
808
|
-
data["category"] = data["category"].apply(process_category)
|
|
809
|
-
data["category"] = data["category"].ffill() # Forward-fill the NaN values
|
|
810
|
-
|
|
811
|
-
# Delete category headers
|
|
812
|
-
# Keep rows where the number of NaNs is less than the total number of columns minus 2
|
|
813
|
-
data = data[data.isna().sum(axis=1) < len(data.columns) - 2]
|
|
814
|
-
# Rename columns
|
|
815
|
-
data.columns = [
|
|
816
|
-
"code",
|
|
817
|
-
"scheme",
|
|
818
|
-
"isin_growth_dividend",
|
|
819
|
-
"isin_reinvestment",
|
|
820
|
-
"nav",
|
|
821
|
-
"repurchase_price",
|
|
822
|
-
"sale_price",
|
|
823
|
-
"date",
|
|
824
|
-
"category",
|
|
825
|
-
]
|
|
826
|
-
|
|
827
|
-
# Convert 'date' to datetime format
|
|
828
|
-
data["date"] = pd.to_datetime(data["date"], format="%d-%b-%Y")
|
|
829
|
-
|
|
830
|
-
# Change 'isin' to NaN if it does not start with 'IN'
|
|
831
|
-
data["isin_growth_dividend"] = data["isin_growth_dividend"].apply(
|
|
832
|
-
lambda x: x if pd.notna(x) and x.startswith("IN") else pd.NA
|
|
833
|
-
)
|
|
834
|
-
data["isin_reinvestment"] = data["isin_reinvestment"].apply(
|
|
835
|
-
lambda x: x if pd.notna(x) and x.startswith("IN") else pd.NA
|
|
836
|
-
)
|
|
837
|
-
|
|
838
|
-
# Filter rows that have 'isin' to exclude unclaimed dividend info
|
|
839
|
-
data = data[data["isin_growth_dividend"].notna() | data["isin_reinvestment"].notna()]
|
|
840
|
-
|
|
841
|
-
# Save DataFrame to CSV
|
|
934
|
+
data = parse_amfi_nav_csv(content)
|
|
842
935
|
save_pandas_in_csv_out(data, dest_file, index=True)
|
|
843
936
|
|
|
844
937
|
get_bootlick_logger().log_info(
|
|
@@ -860,16 +953,6 @@ def save_mf_data(p: str, session, save_to_rds=True):
|
|
|
860
953
|
return None
|
|
861
954
|
return dest_file
|
|
862
955
|
|
|
863
|
-
# Custom function to handle the logic
|
|
864
|
-
def process_category(value):
|
|
865
|
-
try:
|
|
866
|
-
# Check if the value can be coerced into a numeric type or is an empty string
|
|
867
|
-
if value == "" or not np.isnan(pd.to_numeric(value, errors="coerce")):
|
|
868
|
-
return np.nan # Replace with NaN
|
|
869
|
-
except ValueError:
|
|
870
|
-
pass
|
|
871
|
-
return value # Keep the original value if it cannot be coerced
|
|
872
|
-
|
|
873
956
|
try:
|
|
874
957
|
outfolder = get_dynamic_config().get("daily_prices")
|
|
875
958
|
if outfolder is None:
|
|
@@ -907,10 +990,12 @@ def save_mf_data(p: str, session, save_to_rds=True):
|
|
|
907
990
|
f"Processing date: {p}\n\n"
|
|
908
991
|
f"Please ensure the CSV file is downloaded first using download mode before processing.",
|
|
909
992
|
)
|
|
910
|
-
return
|
|
993
|
+
return False
|
|
911
994
|
else:
|
|
912
995
|
# In download mode, download the file
|
|
913
996
|
dest_file = download_csv(url, dest_file)
|
|
997
|
+
if dest_file is None or not file_exists_and_valid(dest_file, min_size=1000):
|
|
998
|
+
return False
|
|
914
999
|
if not save_to_rds or dest_file is None or not file_exists_and_valid(dest_file, min_size=1000):
|
|
915
1000
|
return
|
|
916
1001
|
if file_exists_and_valid(dest_file):
|
|
@@ -1021,6 +1106,7 @@ def save_mf_data(p: str, session, save_to_rds=True):
|
|
|
1021
1106
|
f"Unable to process mutual fund data for {p}",
|
|
1022
1107
|
f"{traceback.format_exc()}",
|
|
1023
1108
|
)
|
|
1109
|
+
return False
|
|
1024
1110
|
|
|
1025
1111
|
|
|
1026
1112
|
def save_option_data_old(p: str, session, save_to_rds: bool = True):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|