ns-devel
commited on
Commit
•
38171fa
1
Parent(s):
b9267ce
Text2SQL app
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .gitignore +61 -0
- Dockerfile +11 -0
- core/__init__.py +0 -0
- core/admin.py +16 -0
- core/apps.py +6 -0
- core/clients/__init__.py +0 -0
- core/clients/mutual_fund.py +267 -0
- core/clients/stock.py +206 -0
- core/constants.py +4 -0
- core/cron.py +9 -0
- core/management/commands/text2sql_eval.py +50 -0
- core/management/commands/update_mf.py +92 -0
- core/mfrating/score_calculator.py +235 -0
- core/middleware.py +55 -0
- core/migrations/0001_initial.py +41 -0
- core/migrations/0002_rename_fund_house_mutualfund_fund_name.py +18 -0
- core/migrations/0002_stock.py +41 -0
- core/migrations/0003_alter_mutualfund_fund_name.py +18 -0
- core/migrations/0003_alter_stock_link.py +18 -0
- core/migrations/0004_stock_isin_number.py +18 -0
- core/migrations/0005_merge_20231211_0610.py +13 -0
- core/migrations/0006_mutualfund_crisil_rank.py +18 -0
- core/migrations/0007_merge_20231214_1924.py +13 -0
- core/migrations/0008_mutualfund_aum.py +18 -0
- core/migrations/0009_mutualfund_expense_ratio_mutualfund_return_m12_and_more.py +106 -0
- core/migrations/0010_remove_mfholdings_nav_mutualfund_nav.py +22 -0
- core/migrations/0011_remove_mfholdings_maturity_date_and_more.py +22 -0
- core/migrations/0012_alter_mfholdings_isin_number.py +18 -0
- core/migrations/0013_alter_mfholdings_country_alter_mfholdings_currency_and_more.py +63 -0
- core/migrations/0014_alter_mfholdings_currency_and_more.py +48 -0
- core/migrations/0015_rename_security_name_mfholdings_holding_name_and_more.py +22 -0
- core/migrations/__init__.py +0 -0
- core/models.py +100 -0
- core/test_models.py +42 -0
- core/tests/__init__.py +0 -0
- core/tests/data.py +339 -0
- core/tests/test_scores.py +101 -0
- core/text2sql/__init__.py +0 -0
- core/text2sql/eval_queries.py +86 -0
- core/text2sql/handler.py +26 -0
- core/text2sql/ml_models.py +24 -0
- core/text2sql/prompt.py +69 -0
- core/urls.py +7 -0
- core/views.py +30 -0
- data_pipeline/__init__.py +0 -0
- data_pipeline/admin.py +3 -0
- data_pipeline/apps.py +6 -0
- data_pipeline/interfaces/__init__.py +0 -0
- data_pipeline/interfaces/api_client.py +36 -0
- data_pipeline/interfaces/test_api_client.py +35 -0
.gitignore
ADDED
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# These are some examples of commonly ignored file patterns.
|
2 |
+
# You should customize this list as applicable to your project.
|
3 |
+
# Learn more about .gitignore:
|
4 |
+
# https://www.atlassian.com/git/tutorials/saving-changes/gitignore
|
5 |
+
|
6 |
+
# Node artifact files
|
7 |
+
node_modules/
|
8 |
+
dist/
|
9 |
+
|
10 |
+
# Compiled Java class files
|
11 |
+
*.class
|
12 |
+
|
13 |
+
# Compiled Python bytecode
|
14 |
+
*.py[cod]
|
15 |
+
|
16 |
+
# Log files
|
17 |
+
*.log
|
18 |
+
|
19 |
+
# Package files
|
20 |
+
*.jar
|
21 |
+
|
22 |
+
# Maven
|
23 |
+
target/
|
24 |
+
dist/
|
25 |
+
|
26 |
+
# JetBrains IDE
|
27 |
+
.idea/
|
28 |
+
|
29 |
+
# Unit test reports
|
30 |
+
TEST*.xml
|
31 |
+
|
32 |
+
# Generated by MacOS
|
33 |
+
.DS_Store
|
34 |
+
|
35 |
+
# Generated by Windows
|
36 |
+
Thumbs.db
|
37 |
+
|
38 |
+
# Applications
|
39 |
+
*.app
|
40 |
+
*.exe
|
41 |
+
*.war
|
42 |
+
|
43 |
+
# Large media files
|
44 |
+
*.mp4
|
45 |
+
*.tiff
|
46 |
+
*.avi
|
47 |
+
*.flv
|
48 |
+
*.mov
|
49 |
+
*.wmv
|
50 |
+
# These are some examples of commonly ignored file patterns.
|
51 |
+
# You should customize this list as applicable to your project.
|
52 |
+
# Learn more about .gitignore:
|
53 |
+
# https://www.atlassian.com/git/tutorials/saving-changes/gitignore
|
54 |
+
|
55 |
+
venv/
|
56 |
+
.venv/
|
57 |
+
|
58 |
+
**/__pycache__/
|
59 |
+
data/*
|
60 |
+
env.sh
|
61 |
+
db.sqlite3
|
Dockerfile
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM python:3.11
|
2 |
+
|
3 |
+
WORKDIR /code
|
4 |
+
|
5 |
+
COPY ./requirements.txt /code/requirements.txt
|
6 |
+
|
7 |
+
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
|
8 |
+
|
9 |
+
COPY . .
|
10 |
+
|
11 |
+
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
|
core/__init__.py
ADDED
File without changes
|
core/admin.py
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
This file is used to register the models in the admin panel.
|
3 |
+
"""
|
4 |
+
|
5 |
+
from django.contrib import admin
|
6 |
+
from core.models import MutualFund, Stock
|
7 |
+
|
8 |
+
|
9 |
+
@admin.register(MutualFund)
|
10 |
+
class MutualFundAdmin(admin.ModelAdmin):
|
11 |
+
list_display = ("id", "rank", "fund_name", "isin_number", "security_id")
|
12 |
+
|
13 |
+
|
14 |
+
@admin.register(Stock)
|
15 |
+
class StockAdmin(admin.ModelAdmin):
|
16 |
+
pass
|
core/apps.py
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from django.apps import AppConfig
|
2 |
+
|
3 |
+
|
4 |
+
class CoreConfig(AppConfig):
|
5 |
+
default_auto_field = "django.db.models.BigAutoField"
|
6 |
+
name = "core"
|
core/clients/__init__.py
ADDED
File without changes
|
core/clients/mutual_fund.py
ADDED
@@ -0,0 +1,267 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
|
3 |
+
"""
|
4 |
+
import logging
|
5 |
+
|
6 |
+
from django.conf import settings
|
7 |
+
|
8 |
+
import requests
|
9 |
+
from bs4 import BeautifulSoup
|
10 |
+
from core.models import MutualFund
|
11 |
+
from core.constants import MONEYCONTROL_TOPFUNDS_URL
|
12 |
+
from data_pipeline.interfaces.api_client import DataClient
|
13 |
+
|
14 |
+
|
15 |
+
logger = logging.getLogger(__name__)
|
16 |
+
|
17 |
+
|
18 |
+
settings.MORNINGSTAR_API_HEADERS = {
|
19 |
+
"X-RapidAPI-Key": settings.MORNINGSTAR_KEY,
|
20 |
+
"X-RapidAPI-Host": settings.MORNINGSTAR_HOST,
|
21 |
+
}
|
22 |
+
|
23 |
+
|
24 |
+
class MFList(DataClient):
|
25 |
+
model = MutualFund
|
26 |
+
|
27 |
+
# Monring Star List MF url
|
28 |
+
api_url = "https://lt.morningstar.com/api/rest.svc/g9vi2nsqjb/security/screener?page=1&pageSize=15000&sortOrder=name%20asc&outputType=json&version=1&languageId=en¤cyId=INR&universeIds=FOIND%24%24ALL%7CFCIND%24%24ALL&securityDataPoints=secId%2ClegalName%2CclosePrice%2CclosePriceDate%2Cyield_M12%2CongoingCharge%2CcategoryName%2CMedalist_RatingNumber%2CstarRatingM255%2CreturnD1%2CreturnW1%2CreturnM1%2CreturnM3%2CreturnM6%2CreturnM0%2CreturnM12%2CreturnM36%2CreturnM60%2CreturnM120%2CmaxFrontEndLoad%2CmanagerTenure%2CmaxDeferredLoad%2CexpenseRatio%2Cisin%2CinitialPurchase%2CfundTnav%2CequityStyleBox%2CbondStyleBox%2CaverageMarketCapital%2CaverageCreditQualityCode%2CeffectiveDuration%2CmorningstarRiskM255%2CalphaM36%2CbetaM36%2Cr2M36%2CstandardDeviationM36%2CsharpeM36%2CtrackRecordExtension&filters=&term="
|
29 |
+
|
30 |
+
def __init__(self) -> None:
|
31 |
+
self.api_response = None
|
32 |
+
self.transformed_data = None
|
33 |
+
|
34 |
+
def extract(self):
|
35 |
+
|
36 |
+
super().extract()
|
37 |
+
|
38 |
+
logger.info("Calling Morningstar API")
|
39 |
+
response = requests.get(self.api_url)
|
40 |
+
|
41 |
+
# Check if the request was successful (status code 200)
|
42 |
+
if response.status_code == 200:
|
43 |
+
|
44 |
+
# Parse JSON response
|
45 |
+
self.api_response = response.json()
|
46 |
+
logger.info(
|
47 |
+
f'Morningstar API response received {len(self.api_response["rows"])} funds'
|
48 |
+
)
|
49 |
+
|
50 |
+
else:
|
51 |
+
logger.info("Received status code: {response.status_code}")
|
52 |
+
logger.info(response.json())
|
53 |
+
|
54 |
+
def transform(self):
|
55 |
+
"""
|
56 |
+
Transform the data to the format required by the model
|
57 |
+
"""
|
58 |
+
|
59 |
+
super().transform()
|
60 |
+
|
61 |
+
self.transformed_data = [
|
62 |
+
{
|
63 |
+
"fund_name": fund["legalName"],
|
64 |
+
"isin_number": fund.get("isin"),
|
65 |
+
"security_id": fund["secId"],
|
66 |
+
"data": {"list_info": fund},
|
67 |
+
}
|
68 |
+
for fund in self.api_response["rows"]
|
69 |
+
]
|
70 |
+
|
71 |
+
def load(self):
|
72 |
+
"""
|
73 |
+
Load the data into the database
|
74 |
+
"""
|
75 |
+
|
76 |
+
create_count = 0
|
77 |
+
update_count = 0
|
78 |
+
for data_dict in self.transformed_data:
|
79 |
+
try:
|
80 |
+
mf = self.model.objects.get(isin_number=data_dict["isin_number"])
|
81 |
+
mf.data.update(data_dict["data"])
|
82 |
+
mf.save()
|
83 |
+
update_count += 1
|
84 |
+
except self.model.DoesNotExist:
|
85 |
+
mf = self.model(**data_dict)
|
86 |
+
mf.save()
|
87 |
+
create_count += 1
|
88 |
+
|
89 |
+
logger.info(
|
90 |
+
"Created %s records; Updated %s records", create_count, update_count
|
91 |
+
)
|
92 |
+
|
93 |
+
|
94 |
+
class MFQuote(DataClient):
|
95 |
+
model = MutualFund
|
96 |
+
|
97 |
+
# Monring Star get quote url
|
98 |
+
api_url = f"https://{settings.MORNINGSTAR_HOST}/etf/get-quote"
|
99 |
+
|
100 |
+
def __init__(self, isin) -> None:
|
101 |
+
self.api_response = None # {"quotes": None, "holdings": None}
|
102 |
+
self.transformed_data = None
|
103 |
+
self.isin = isin
|
104 |
+
self.mf = self.model.objects.get(isin_number=self.isin)
|
105 |
+
|
106 |
+
def extract(self):
|
107 |
+
|
108 |
+
logger.info(f"Calling Morningstar Quote API for quotes with isin {self.isin}")
|
109 |
+
querystring = {"securityId": self.mf.security_id}
|
110 |
+
|
111 |
+
response = requests.get(
|
112 |
+
self.api_url, headers=settings.MORNINGSTAR_API_HEADERS, params=querystring
|
113 |
+
)
|
114 |
+
|
115 |
+
# Check if the request was successful (status code 200)
|
116 |
+
if response.status_code == 200:
|
117 |
+
# Parse JSON response
|
118 |
+
self.api_response = response.json()
|
119 |
+
else:
|
120 |
+
logger.info(f"API response: %s", response.status_code)
|
121 |
+
response.raise_for_status()
|
122 |
+
|
123 |
+
def load(self):
|
124 |
+
self.mf.data.update({"quotes": self.transformed_data})
|
125 |
+
self.mf.save()
|
126 |
+
logger.info(f"Successfully stored data of quotes for {self.mf.fund_name}")
|
127 |
+
|
128 |
+
|
129 |
+
class MFHoldings(DataClient):
|
130 |
+
model = MutualFund
|
131 |
+
api_url = f"https://{settings.MORNINGSTAR_HOST}/etf/portfolio/get-holdings"
|
132 |
+
|
133 |
+
def __init__(self, isin) -> None:
|
134 |
+
self.api_response = None
|
135 |
+
self.transformed_data = None
|
136 |
+
self.isin = isin
|
137 |
+
self.mf = self.model.objects.get(isin_number=self.isin)
|
138 |
+
|
139 |
+
def extract(self):
|
140 |
+
|
141 |
+
querystring = {"securityId": self.mf.security_id}
|
142 |
+
|
143 |
+
response = requests.get(
|
144 |
+
self.api_url, headers=settings.MORNINGSTAR_API_HEADERS, params=querystring
|
145 |
+
)
|
146 |
+
|
147 |
+
# Check if the request was successful (status code 200)
|
148 |
+
if response.status_code == 200:
|
149 |
+
# Parse JSON response
|
150 |
+
self.api_response = response.json()
|
151 |
+
else:
|
152 |
+
logger.info(f"received status code {response.status_code} for {self.isin}")
|
153 |
+
logger.debug(response.content)
|
154 |
+
response.raise_for_status()
|
155 |
+
|
156 |
+
def load(self):
|
157 |
+
self.mf.data.update({"holdings": self.transformed_data})
|
158 |
+
self.mf.save()
|
159 |
+
logger.info(f"Successfully stored data of holdings for {self.mf.fund_name}")
|
160 |
+
|
161 |
+
|
162 |
+
class MFRiskMeasures(DataClient):
|
163 |
+
model = MutualFund
|
164 |
+
api_url = (
|
165 |
+
f"https://{settings.MORNINGSTAR_HOST}/etf/risk/get-risk-volatility-measures"
|
166 |
+
)
|
167 |
+
|
168 |
+
def __init__(self, isin) -> None:
|
169 |
+
self.api_response = None
|
170 |
+
self.isin = isin
|
171 |
+
self.mf = self.model.objects.get(isin_number=self.isin)
|
172 |
+
|
173 |
+
def extract(self):
|
174 |
+
|
175 |
+
querystring = {"securityId": self.mf.security_id}
|
176 |
+
|
177 |
+
response = requests.get(
|
178 |
+
self.api_url, headers=settings.MORNINGSTAR_API_HEADERS, params=querystring
|
179 |
+
)
|
180 |
+
|
181 |
+
# Check if the request was successful (status code 200)
|
182 |
+
if response.status_code == 200:
|
183 |
+
# Parse JSON response
|
184 |
+
self.api_response = response.json()
|
185 |
+
else:
|
186 |
+
logger.info(response.json())
|
187 |
+
response.raise_for_status()
|
188 |
+
|
189 |
+
def load(self):
|
190 |
+
self.mf.data.update({"risk_measures": self.transformed_data})
|
191 |
+
self.mf.save()
|
192 |
+
logger.info(
|
193 |
+
f"Successfully stored data of risk measures for {self.mf.fund_name}"
|
194 |
+
)
|
195 |
+
|
196 |
+
|
197 |
+
class MFRanking(DataClient):
|
198 |
+
|
199 |
+
api_url = MONEYCONTROL_TOPFUNDS_URL
|
200 |
+
model = MutualFund
|
201 |
+
|
202 |
+
def __init__(self) -> None:
|
203 |
+
self.api_response = None
|
204 |
+
self.transformed_data = None
|
205 |
+
|
206 |
+
def extract(self) -> None:
|
207 |
+
"""
|
208 |
+
Fetches the top mutual funds from MoneyControl website based on their returns and
|
209 |
+
returns a tuple containing lists of fund names, fund types, CRISIL ranks,
|
210 |
+
INF numbers, and AUM data of top mutual funds.
|
211 |
+
"""
|
212 |
+
super().extract()
|
213 |
+
|
214 |
+
logger.info("Fetching top mutual funds from MoneyControl website")
|
215 |
+
response = requests.get(self.api_url)
|
216 |
+
|
217 |
+
# Check if the request was successful (status code 200)
|
218 |
+
response.raise_for_status()
|
219 |
+
|
220 |
+
soup = BeautifulSoup(response.text, "html.parser")
|
221 |
+
|
222 |
+
# Find all rows containing fund information
|
223 |
+
fund_rows = soup.find_all("tr", class_=lambda x: x and "INF" in x)
|
224 |
+
logger.info("Found %s rows", len(fund_rows))
|
225 |
+
|
226 |
+
fund_details = []
|
227 |
+
|
228 |
+
# Extract fund name from each row of sponsored funds
|
229 |
+
for row in fund_rows:
|
230 |
+
columns = row.find_all("td")
|
231 |
+
fund_name = columns[0].text.strip()
|
232 |
+
fund_type = columns[2].text.strip()
|
233 |
+
crisil_rank = columns[3].text.strip()
|
234 |
+
aum = columns[4].text.strip()
|
235 |
+
isin_number = row["class"][0]
|
236 |
+
|
237 |
+
fund_details.append(
|
238 |
+
{
|
239 |
+
"fund_name": fund_name,
|
240 |
+
"fund_type": fund_type,
|
241 |
+
"crisil_rank": crisil_rank,
|
242 |
+
"isin_number": isin_number,
|
243 |
+
"aum": aum,
|
244 |
+
}
|
245 |
+
)
|
246 |
+
|
247 |
+
self.api_response = fund_details
|
248 |
+
|
249 |
+
def load(self) -> None:
|
250 |
+
"""
|
251 |
+
Load the data into the database
|
252 |
+
"""
|
253 |
+
|
254 |
+
# clear the rank field
|
255 |
+
MutualFund.objects.exclude(rank=None).update(rank=None)
|
256 |
+
|
257 |
+
for rank, fund_details in enumerate(self.transformed_data, 1):
|
258 |
+
mf = MutualFund.objects.get(isin_number=fund_details["isin_number"])
|
259 |
+
mf.crisil_rank = (
|
260 |
+
fund_details["crisil_rank"] if fund_details["crisil_rank"] != "-" else 0
|
261 |
+
)
|
262 |
+
mf.rank = rank
|
263 |
+
mf.aum = float(fund_details["aum"].replace(",", ""))
|
264 |
+
mf.save()
|
265 |
+
logger.info(
|
266 |
+
f"Updated {rank=} {mf.fund_name} | {fund_details=} {fund_details['crisil_rank']=} {fund_details['aum']=}"
|
267 |
+
)
|
core/clients/stock.py
ADDED
@@ -0,0 +1,206 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
This module contains the client classes for retrieving stock data from various sources.
|
3 |
+
"""
|
4 |
+
|
5 |
+
import time
|
6 |
+
import logging
|
7 |
+
|
8 |
+
import requests
|
9 |
+
from bs4 import BeautifulSoup
|
10 |
+
|
11 |
+
from django.conf import settings
|
12 |
+
from core.models import Stock
|
13 |
+
from data_pipeline.interfaces.api_client import DataClient
|
14 |
+
from core.constants import MONEYCONTROL_TOPSTOCKS_URL
|
15 |
+
|
16 |
+
logger = logging.getLogger(__name__)
|
17 |
+
|
18 |
+
|
19 |
+
class StockRankings(DataClient):
|
20 |
+
""" """
|
21 |
+
|
22 |
+
model = Stock
|
23 |
+
api_url = MONEYCONTROL_TOPSTOCKS_URL
|
24 |
+
|
25 |
+
def __init__(self) -> None:
|
26 |
+
self.api_response = None
|
27 |
+
self.transformed_data = None
|
28 |
+
|
29 |
+
def extract(self) -> None:
|
30 |
+
|
31 |
+
logger.info("Fetching data from %s", self.api_url)
|
32 |
+
with requests.Session() as session:
|
33 |
+
try:
|
34 |
+
response = session.get(self.api_url)
|
35 |
+
response.raise_for_status() # Raise an HTTPError for bad responses (4xx or 5xx)
|
36 |
+
except requests.exceptions.RequestException as e:
|
37 |
+
logger.error(f"Error fetching data from {self.api_url}: {e}")
|
38 |
+
return
|
39 |
+
|
40 |
+
soup = BeautifulSoup(response.text, "html.parser")
|
41 |
+
|
42 |
+
# Find the table containing stock information
|
43 |
+
table = soup.find("table", {"id": "indicesTable"})
|
44 |
+
if not table:
|
45 |
+
logger.warning(
|
46 |
+
"Table with id 'indicesTable' not found for stock during scraping ranks."
|
47 |
+
)
|
48 |
+
raise Exception(
|
49 |
+
"Table with id 'indicesTable' not found for stock during scraping ranks."
|
50 |
+
)
|
51 |
+
|
52 |
+
data = []
|
53 |
+
rows = table.find_all("tr")
|
54 |
+
logger.info(f"Found {len(rows)} rows in the table")
|
55 |
+
isin_fails = 0
|
56 |
+
# Extract data from each row
|
57 |
+
for idx, row in enumerate(rows, start=1):
|
58 |
+
columns = row.find_all("td")
|
59 |
+
isin_number = None
|
60 |
+
if columns:
|
61 |
+
link = columns[0].find("a").get("href") if columns[0] else None
|
62 |
+
if link is not None:
|
63 |
+
try:
|
64 |
+
logger.info(f"Fetching stock details from link {link}")
|
65 |
+
response = session.get(link)
|
66 |
+
time.sleep(2)
|
67 |
+
response.raise_for_status()
|
68 |
+
soup = BeautifulSoup(response.text, "html.parser")
|
69 |
+
isin_element = soup.select_one(
|
70 |
+
'li.clearfix span:contains("ISIN") + p'
|
71 |
+
)
|
72 |
+
if isin_element:
|
73 |
+
isin_number = isin_element.get_text(strip=True)
|
74 |
+
else:
|
75 |
+
isin_fails += 1
|
76 |
+
logger.warning(f"ISIN not found for link {link}")
|
77 |
+
except requests.exceptions.RequestException as e:
|
78 |
+
logger.exception(
|
79 |
+
f"Error fetching ISIN from link {link}: {e}"
|
80 |
+
)
|
81 |
+
data.append(
|
82 |
+
{
|
83 |
+
"name": columns[0].get_text(strip=True),
|
84 |
+
"ltp": columns[1].get_text(strip=True),
|
85 |
+
"link": link,
|
86 |
+
"volume": columns[4].get_text(strip=True),
|
87 |
+
"percentage_change": columns[2].get_text(strip=True),
|
88 |
+
"price_change": columns[3].get_text(strip=True),
|
89 |
+
"rank": idx,
|
90 |
+
"isin_number": isin_number,
|
91 |
+
}
|
92 |
+
)
|
93 |
+
logger.info(f"ISIN not found for {isin_fails} stocks out of {len(rows)}")
|
94 |
+
|
95 |
+
self.api_response = data
|
96 |
+
|
97 |
+
def load(self) -> None:
|
98 |
+
"""
|
99 |
+
Load the data into the database
|
100 |
+
"""
|
101 |
+
logger.info("Loading ranking data into the database...")
|
102 |
+
# clear the rank field
|
103 |
+
Stock.objects.exclude(rank=None).update(rank=None)
|
104 |
+
|
105 |
+
for rank, stock_details in enumerate(self.transformed_data, 1):
|
106 |
+
try:
|
107 |
+
stock = Stock.objects.get(isin_number=stock_details["isin_number"])
|
108 |
+
except Stock.DoesNotExist:
|
109 |
+
logger.warning(
|
110 |
+
f"No matching stock found for ISIN: {stock_details['isin_number']} creating new object..."
|
111 |
+
)
|
112 |
+
stock = Stock.objects.create(data={"stock_rank": stock_details})
|
113 |
+
|
114 |
+
else:
|
115 |
+
stock.data.update({"stock_rank": stock_details})
|
116 |
+
|
117 |
+
stock.name = stock_details["name"]
|
118 |
+
stock.ltp = stock_details["ltp"]
|
119 |
+
stock.percentage_change = stock_details["percentage_change"]
|
120 |
+
stock.price_change = stock_details["price_change"]
|
121 |
+
stock.link = stock_details["link"]
|
122 |
+
stock.volume = stock_details["volume"]
|
123 |
+
stock.isin_number = stock_details["isin_number"]
|
124 |
+
stock.rank = stock_details["rank"]
|
125 |
+
stock.save()
|
126 |
+
|
127 |
+
logger.info(
|
128 |
+
f"Saved {rank=} {stock.name} | {stock_details=} {stock_details['isin_number']=}"
|
129 |
+
)
|
130 |
+
|
131 |
+
|
132 |
+
class StockDetails(DataClient):
|
133 |
+
"""
|
134 |
+
Retrieves and updates stock details from the Morningstar API.
|
135 |
+
"""
|
136 |
+
|
137 |
+
model = Stock
|
138 |
+
api_url = f"https://{settings.MORNINGSTAR_HOST}/stock/get-detail"
|
139 |
+
|
140 |
+
def __init__(self, perf_id: str, isin_number: str) -> None:
|
141 |
+
"""
|
142 |
+
Initializes the StockDetails object.
|
143 |
+
|
144 |
+
Args:
|
145 |
+
perf_id (str): Performance ID of the stock.
|
146 |
+
isin_number (str): ISIN number of the stock.
|
147 |
+
"""
|
148 |
+
if not perf_id:
|
149 |
+
raise ValueError("Performance ID cannot be empty.")
|
150 |
+
if not isin_number:
|
151 |
+
raise ValueError("ISIN number cannot be empty.")
|
152 |
+
|
153 |
+
self.api_response = {"details": None}
|
154 |
+
self.perf_id = perf_id
|
155 |
+
self.isin_number = isin_number
|
156 |
+
|
157 |
+
def _request(self) -> requests.Response:
|
158 |
+
|
159 |
+
querystring = {"PerformanceId": self.perf_id}
|
160 |
+
return requests.get(
|
161 |
+
self.api_url,
|
162 |
+
headers=settings.MORNINGSTAR_API_HEADERS,
|
163 |
+
params=querystring,
|
164 |
+
)
|
165 |
+
|
166 |
+
def extract(self) -> None:
|
167 |
+
"""
|
168 |
+
Extracts stock details from the Morningstar API.
|
169 |
+
"""
|
170 |
+
|
171 |
+
response = self._request()
|
172 |
+
|
173 |
+
requests_count = 1
|
174 |
+
while response.status_code != 200:
|
175 |
+
if response.status_code == 429:
|
176 |
+
|
177 |
+
logger.info(
|
178 |
+
f"API response: %s. Waiting for %s secs",
|
179 |
+
response.status_code,
|
180 |
+
30 * requests_count,
|
181 |
+
)
|
182 |
+
time.sleep(30 * requests_count)
|
183 |
+
response = self._request()
|
184 |
+
if requests_count > 3:
|
185 |
+
logger.warning(
|
186 |
+
f"API response: %s. Max retries reached", response.status_code
|
187 |
+
)
|
188 |
+
break
|
189 |
+
requests_count += 1
|
190 |
+
|
191 |
+
else:
|
192 |
+
self.api_response["details"] = response.json()
|
193 |
+
logger.info(f"API response: %s", response.status_code)
|
194 |
+
|
195 |
+
def load(self) -> None:
|
196 |
+
"""
|
197 |
+
Loads the retrieved stock details into the database.
|
198 |
+
"""
|
199 |
+
|
200 |
+
stock = Stock.objects.filter(isin_number=self.isin_number).first()
|
201 |
+
if stock is None:
|
202 |
+
logger.warning(f"No matching stock found for ISIN: {self.isin_number}")
|
203 |
+
return
|
204 |
+
stock.data = self.transformed_data
|
205 |
+
stock.save()
|
206 |
+
logger.info(f"Successfully stored data for {stock.isin_number}.")
|
core/constants.py
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
MONEYCONTROL_TOPFUNDS_URL = "https://www.moneycontrol.com/mutual-funds/performance-tracker/returns/large-cap-fund.html"
|
2 |
+
MONEYCONTROL_TOPSTOCKS_URL = "https://www.moneycontrol.com/markets/indian-indices/changeTableData?deviceType=web&exName=N&indicesID=49&selTab=o&subTabOT=d&subTabOPL=cl&selPage=marketTerminal&classic=true"
|
3 |
+
TOPFUNDS_COUNT = 30
|
4 |
+
STOCKS_MAX_RANK = 1000
|
core/cron.py
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from data_pipeline import MFList
|
2 |
+
|
3 |
+
|
4 |
+
def store_mutual_funds():
|
5 |
+
mf_list = MFList()
|
6 |
+
mf_list.extract()
|
7 |
+
mf_list.transform()
|
8 |
+
mf_list.load()
|
9 |
+
print("Stored successfully")
|
core/management/commands/text2sql_eval.py
ADDED
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import logging
|
2 |
+
import time
|
3 |
+
import csv
|
4 |
+
from django.core.management.base import BaseCommand
|
5 |
+
from core.text2sql.handler import QueryDataHandler
|
6 |
+
from core.text2sql.prompt import get_prompt
|
7 |
+
from core.text2sql.eval_queries import queries
|
8 |
+
|
9 |
+
logger = logging.getLogger(__name__)
|
10 |
+
|
11 |
+
class Command(BaseCommand):
|
12 |
+
help = "Morningstar API to save the JSON response to a file which contains secIds with other details"
|
13 |
+
|
14 |
+
def handle(self, *args, **options) -> None:
|
15 |
+
t1 = time.perf_counter()
|
16 |
+
q=[]
|
17 |
+
count =1
|
18 |
+
for query in queries[26:]:
|
19 |
+
print("count: ", query["Query Number"])
|
20 |
+
prompt = get_prompt(query["Query Description"])
|
21 |
+
logger.info(f"Prompt: {prompt}")
|
22 |
+
generated_query, data = QueryDataHandler().get_data_from_query(prompt)
|
23 |
+
print(f"Description: {query['Query Description']}, Query: {query.get('SQL Statement')}, Generated: {generated_query} ")
|
24 |
+
q.append({
|
25 |
+
"Query Number": query["Query Number"],
|
26 |
+
"Complexity Level": query["Complexity Level"],
|
27 |
+
"Description": query["Query Description"],
|
28 |
+
"Query": query.get("SQL Statement", "-"),
|
29 |
+
"Generated": generated_query,
|
30 |
+
})
|
31 |
+
count+=1
|
32 |
+
time.sleep(1)
|
33 |
+
csv_file_path = 'queries_data.csv'
|
34 |
+
|
35 |
+
# Writing data to CSV
|
36 |
+
with open(csv_file_path, 'w', newline='', encoding='utf-8') as csv_file:
|
37 |
+
fieldnames = q[0].keys()
|
38 |
+
print(fieldnames)
|
39 |
+
writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
|
40 |
+
|
41 |
+
# Write the header
|
42 |
+
writer.writeheader()
|
43 |
+
|
44 |
+
# Write the data
|
45 |
+
writer.writerows(q)
|
46 |
+
|
47 |
+
print(f'Data has been written to {csv_file_path}.')
|
48 |
+
self.stdout.write(f"Time taken for evaluation: {time.perf_counter() - t1}")
|
49 |
+
|
50 |
+
|
core/management/commands/update_mf.py
ADDED
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
This command is used to fetch all the data relating to mutual funds and save it to the database.
|
3 |
+
"""
|
4 |
+
|
5 |
+
import logging
|
6 |
+
import time
|
7 |
+
|
8 |
+
from django.core.management.base import BaseCommand
|
9 |
+
from core.constants import MONEYCONTROL_TOPSTOCKS_URL
|
10 |
+
from core.models import MutualFund
|
11 |
+
from core.clients.mutual_fund import (
|
12 |
+
MFList,
|
13 |
+
MFQuote,
|
14 |
+
MFHoldings,
|
15 |
+
MFRiskMeasures,
|
16 |
+
MFRanking,
|
17 |
+
)
|
18 |
+
from core.clients.stock import StockDetails, StockRankings
|
19 |
+
|
20 |
+
logger = logging.getLogger(__name__)
|
21 |
+
|
22 |
+
|
23 |
+
def get_funds_details() -> None:
|
24 |
+
"""
|
25 |
+
Get the details of the top 30 mutual funds and store them in the database.
|
26 |
+
"""
|
27 |
+
|
28 |
+
t1 = time.perf_counter()
|
29 |
+
mutual_funds = MutualFund.objects.exclude(rank=None).order_by("rank")[:30]
|
30 |
+
logger.info(f"{mutual_funds=}")
|
31 |
+
for mf in mutual_funds:
|
32 |
+
# fetching the quotes data from the morningstar api and storing it in the database
|
33 |
+
MFQuote(mf.isin_number).run()
|
34 |
+
time.sleep(2)
|
35 |
+
# fetching the holdings data from the morningstar api and storing it in the database
|
36 |
+
MFHoldings(mf.isin_number).run()
|
37 |
+
time.sleep(2)
|
38 |
+
|
39 |
+
# fetching the risk measures data from the morningstar api and storing it in the database
|
40 |
+
MFRiskMeasures(mf.isin_number).run()
|
41 |
+
time.sleep(2)
|
42 |
+
|
43 |
+
logger.info("Time taken: %s", time.perf_counter() - t1)
|
44 |
+
|
45 |
+
|
46 |
+
def get_stock_details() -> None:
|
47 |
+
"""
|
48 |
+
Retrieves stock details for the top 30 mutual funds and updates the database.
|
49 |
+
"""
|
50 |
+
count = 0
|
51 |
+
t1 = time.perf_counter()
|
52 |
+
mutual_funds = MutualFund.objects.exclude(rank=None).order_by("rank")[:30]
|
53 |
+
|
54 |
+
for mf in mutual_funds:
|
55 |
+
|
56 |
+
try:
|
57 |
+
holdings = (
|
58 |
+
mf.data["holdings"].get("equityHoldingPage", {}).get("holdingList", [])
|
59 |
+
)
|
60 |
+
except KeyError:
|
61 |
+
logger.warning("KeyError for holdings on Mutual Fund %s", mf.isin_number)
|
62 |
+
|
63 |
+
for holding in holdings:
|
64 |
+
performance_id = holding.get("performanceId")
|
65 |
+
isin = holding.get("isin")
|
66 |
+
|
67 |
+
if not performance_id or not isin:
|
68 |
+
logger.warning("Missing performanceId or isin for Mutual Fund %s", isin)
|
69 |
+
MFHoldings(mf.isin_number).run()
|
70 |
+
|
71 |
+
stock_details = StockDetails(performance_id, isin)
|
72 |
+
stock_details.run()
|
73 |
+
count += 1
|
74 |
+
|
75 |
+
logger.info("Processed count: %s", count)
|
76 |
+
logger.info("Time taken by stock details: %s", time.perf_counter() - t1)
|
77 |
+
|
78 |
+
|
79 |
+
class Command(BaseCommand):
|
80 |
+
help = "Morningstar API to save the JSON response to a file which contains secIds with other details"
|
81 |
+
|
82 |
+
def handle(self, *args, **options) -> None:
|
83 |
+
t1 = time.perf_counter()
|
84 |
+
try:
|
85 |
+
MFList().run()
|
86 |
+
MFRanking().run()
|
87 |
+
get_funds_details()
|
88 |
+
StockRankings().run()
|
89 |
+
get_stock_details()
|
90 |
+
except Exception as e:
|
91 |
+
logger.exception(e)
|
92 |
+
self.stdout.write(f"Time taken by: {time.perf_counter() - t1}")
|
core/mfrating/score_calculator.py
ADDED
@@ -0,0 +1,235 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
This module defines a class, MFRating, which provides methods for calculating
|
3 |
+
the weighted rating and overall score for mutual funds based on various parameters.
|
4 |
+
|
5 |
+
"""
|
6 |
+
import logging
|
7 |
+
from typing import List, Dict, Any
|
8 |
+
import numpy as np
|
9 |
+
from django.db.models import Max, Min
|
10 |
+
from core.models import MutualFund, Stock
|
11 |
+
|
12 |
+
|
13 |
+
logger = logging.getLogger(__name__)
|
14 |
+
|
15 |
+
|
16 |
+
class MFRating:
|
17 |
+
"""
|
18 |
+
This class provides methods for calculating the weighted stock rank rating and overall score for mutual funds based on various parameters.
|
19 |
+
"""
|
20 |
+
|
21 |
+
def __init__(self, max_rank: int = 1000) -> None:
|
22 |
+
self.max_rank = max_rank
|
23 |
+
self.scores = {
|
24 |
+
"stock_ranking_score": [10],
|
25 |
+
"crisil_rank_score": [10],
|
26 |
+
"churn_score": [10],
|
27 |
+
"sharperatio_score": [10],
|
28 |
+
"expenseratio_score": [10],
|
29 |
+
"aum_score": [10],
|
30 |
+
"alpha_score": [10],
|
31 |
+
"beta_score": [10],
|
32 |
+
}
|
33 |
+
|
34 |
+
def get_weighted_score(self, values: List[float]) -> float:
|
35 |
+
"""
|
36 |
+
Calculates the weighted rating based on the weights and values provided.
|
37 |
+
"""
|
38 |
+
weights = []
|
39 |
+
values = []
|
40 |
+
for _, (weight, score) in self.scores.items():
|
41 |
+
weights.append(weight)
|
42 |
+
values.append(score)
|
43 |
+
|
44 |
+
return np.average(values, weights=weights)
|
45 |
+
|
46 |
+
def get_rank_rating(self, stock_ranks: List[int]) -> List[float]:
|
47 |
+
"""
|
48 |
+
Calculates the rank rating based on the stock ranks and the maximum rank.
|
49 |
+
"""
|
50 |
+
return [
|
51 |
+
(self.max_rank - (rank if rank else self.max_rank)) / self.max_rank
|
52 |
+
for rank in stock_ranks
|
53 |
+
]
|
54 |
+
|
55 |
+
def get_overall_score(self, **kwargs) -> float:
|
56 |
+
"""
|
57 |
+
It returns the overall weighted score for mutual funds based on various parameters.
|
58 |
+
|
59 |
+
"""
|
60 |
+
|
61 |
+
stock_rankings = self.get_rank_rating(kwargs.get("stock_rankings"))
|
62 |
+
# what np.average do?
|
63 |
+
# Multiply each element in the stock_rankings array by its corresponding weights, then Sum up the results, then divide by the sum of the weights.
|
64 |
+
# data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
65 |
+
# weights = [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
|
66 |
+
#
|
67 |
+
# Multiply each element in the data array by its corresponding weight:
|
68 |
+
# [1*10, 2*9, 3*8, 4*7, 5*6, 6*5, 7*4, 8*3, 9*2, 10*1]
|
69 |
+
# [10, 18, 24, 28, 30, 30, 28, 24, 18, 10]
|
70 |
+
#
|
71 |
+
# Sum up the results:
|
72 |
+
# 10 + 18 + 24 + 28 + 30 + 30 + 28 + 24 + 18 + 10 = 220
|
73 |
+
#
|
74 |
+
# Sum up the weights:
|
75 |
+
# 10 + 9 + 8 + 7 + 6 + 5 + 4 + 3 + 2 + 1 = 55
|
76 |
+
#
|
77 |
+
# Divide the sum of the weighted elements by the sum of the weights:
|
78 |
+
# 220 / 55 = 4.0
|
79 |
+
self.scores["stock_ranking_score"].append(
|
80 |
+
np.average(stock_rankings, weights=kwargs.get("stock_weights"))
|
81 |
+
)
|
82 |
+
self.scores["alpha_score"].append(kwargs.get("alpha", 0) / 100)
|
83 |
+
self.scores["beta_score"].append((2 - kwargs.get("beta", 2)) / 2)
|
84 |
+
self.scores["crisil_rank_score"].append(
|
85 |
+
(kwargs.get("crisil_rank_score", 0)) / 5
|
86 |
+
)
|
87 |
+
self.scores["churn_score"].append(kwargs.get("churn_rate", 0) / 100)
|
88 |
+
self.scores["sharperatio_score"].append(kwargs.get("sharpe_ratio", 0) / 100)
|
89 |
+
self.scores["expenseratio_score"].append(kwargs.get("expense_ratio", 0) / 100)
|
90 |
+
max_aum, min_aum, aum = kwargs.get("aum_score", (1, 0, 0))
|
91 |
+
self.scores["aum_score"].append((aum - min_aum) / (max_aum - min_aum))
|
92 |
+
# Calculate the overall rating using weighted sum
|
93 |
+
|
94 |
+
return self.get_weighted_score(self.scores)
|
95 |
+
|
96 |
+
|
97 |
+
class MutualFundScorer:
|
98 |
+
def __init__(self) -> None:
|
99 |
+
self.mf_scores = []
|
100 |
+
|
101 |
+
def _get_stock_ranks(self, isin_ids: List[str]) -> List[int]:
|
102 |
+
"""Get stock ranks based on ISIN ids."""
|
103 |
+
|
104 |
+
return list(
|
105 |
+
Stock.objects.filter(isin_number__in=isin_ids)
|
106 |
+
.order_by("rank")
|
107 |
+
.values_list("rank", "isin_number")
|
108 |
+
)
|
109 |
+
|
110 |
+
def _get_mutual_funds(self) -> List[MutualFund]:
|
111 |
+
"""Get a list of top 30 mutual funds based on rank."""
|
112 |
+
|
113 |
+
return MutualFund.objects.exclude(rank=None).order_by("rank")[:30]
|
114 |
+
|
115 |
+
def _get_risk_measure(
|
116 |
+
self, risk_measures: Dict[str, Any], key: str, year: str
|
117 |
+
) -> float:
|
118 |
+
"""
|
119 |
+
Get value of the specified key from the risk_measures dictionary for the given year.
|
120 |
+
"""
|
121 |
+
try:
|
122 |
+
value = risk_measures.get(year, {}).get(key, 0)
|
123 |
+
return float(value)
|
124 |
+
except (TypeError, ValueError):
|
125 |
+
return 0
|
126 |
+
|
127 |
+
def _get_most_non_null_key(self, key, mutual_funds):
|
128 |
+
"""
|
129 |
+
Get the year with the maximum number of non-None values for the specified key
|
130 |
+
within the given mutual funds.
|
131 |
+
"""
|
132 |
+
year_counts = {
|
133 |
+
"for15Year": 0,
|
134 |
+
"for10Year": 0,
|
135 |
+
"for5Year": 0,
|
136 |
+
"for3Year": 0,
|
137 |
+
"for1Year": 0,
|
138 |
+
}
|
139 |
+
|
140 |
+
for mf in mutual_funds:
|
141 |
+
risk_measures = mf.data["risk_measures"].get("fundRiskVolatility", {})
|
142 |
+
|
143 |
+
for year in year_counts:
|
144 |
+
if risk_measures.get(year, {}).get(key) is not None:
|
145 |
+
year_counts[year] += 1
|
146 |
+
|
147 |
+
most_non_null_year = max(year_counts, key=year_counts.get)
|
148 |
+
return most_non_null_year
|
149 |
+
|
150 |
+
def get_scores(self) -> List[Dict[str, Any]]:
|
151 |
+
"""Calculate scores for mutual funds and return the results."""
|
152 |
+
|
153 |
+
logger.info("Calculating scores for mutual funds...")
|
154 |
+
max_aum = MutualFund.objects.exclude(rank=None).aggregate(max_price=Max("aum"))[
|
155 |
+
"max_price"
|
156 |
+
]
|
157 |
+
min_aum = MutualFund.objects.exclude(rank=None).aggregate(min_price=Min("aum"))[
|
158 |
+
"min_price"
|
159 |
+
]
|
160 |
+
mutual_funds = self._get_mutual_funds()
|
161 |
+
|
162 |
+
# Get the year with the maximum number of non-None values for sharpeRatio, alpha and beta
|
163 |
+
sharpe_ratio_year = self._get_most_non_null_key("sharpeRatio", mutual_funds)
|
164 |
+
alpha_year = self._get_most_non_null_key("alpha", mutual_funds)
|
165 |
+
beta_year = self._get_most_non_null_key("beta", mutual_funds)
|
166 |
+
for mf in mutual_funds:
|
167 |
+
mf_rating = MFRating(
|
168 |
+
max_rank=1000,
|
169 |
+
)
|
170 |
+
logger.info(f"Processing mutual fund: %s", mf.fund_name)
|
171 |
+
holdings = (
|
172 |
+
mf.data.get("holdings", {})
|
173 |
+
.get("equityHoldingPage", {})
|
174 |
+
.get("holdingList", [])
|
175 |
+
)
|
176 |
+
portfolio_holding_weights = {
|
177 |
+
holding.get("isin"): (
|
178 |
+
holding.get("weighting") if holding.get("weighting") else 0
|
179 |
+
)
|
180 |
+
for holding in holdings
|
181 |
+
if holding.get("isin")
|
182 |
+
}
|
183 |
+
stock_ranks_and_weights = [
|
184 |
+
(rank, portfolio_holding_weights[isin])
|
185 |
+
for rank, isin in self._get_stock_ranks(
|
186 |
+
portfolio_holding_weights.keys()
|
187 |
+
)
|
188 |
+
]
|
189 |
+
stock_ranks, stock_weights = zip(*stock_ranks_and_weights)
|
190 |
+
sharpe_ratio = self._get_risk_measure(
|
191 |
+
mf.data["risk_measures"].get("fundRiskVolatility", {}),
|
192 |
+
"sharpeRatio",
|
193 |
+
sharpe_ratio_year,
|
194 |
+
)
|
195 |
+
alpha = self._get_risk_measure(
|
196 |
+
mf.data["risk_measures"].get("fundRiskVolatility", {}),
|
197 |
+
"alpha",
|
198 |
+
alpha_year,
|
199 |
+
)
|
200 |
+
beta = self._get_risk_measure(
|
201 |
+
mf.data["risk_measures"].get("fundRiskVolatility", {}),
|
202 |
+
"beta",
|
203 |
+
beta_year,
|
204 |
+
)
|
205 |
+
overall_score = mf_rating.get_overall_score(
|
206 |
+
stock_rankings=stock_ranks,
|
207 |
+
stock_weights=stock_weights,
|
208 |
+
churn_rate=mf.data["quotes"]["lastTurnoverRatio"]
|
209 |
+
if mf.data["quotes"].get("lastTurnoverRatio")
|
210 |
+
else 0,
|
211 |
+
sharpe_ratio=sharpe_ratio,
|
212 |
+
expense_ratio=mf.data["quotes"]["expenseRatio"],
|
213 |
+
crisil_rank_score=mf.crisil_rank,
|
214 |
+
aum_score=(max_aum, min_aum, mf.aum),
|
215 |
+
alpha=alpha,
|
216 |
+
beta=beta,
|
217 |
+
)
|
218 |
+
|
219 |
+
self.mf_scores.append(
|
220 |
+
{
|
221 |
+
"isin": mf.isin_number,
|
222 |
+
"name": mf.fund_name,
|
223 |
+
"rank": mf.rank,
|
224 |
+
"sharpe_ratio": round(sharpe_ratio, 4),
|
225 |
+
"churn_rate": mf.data["quotes"].get("lastTurnoverRatio", 0),
|
226 |
+
"expense_ratio": mf.data["quotes"].get("expenseRatio", 0),
|
227 |
+
"aum": mf.aum,
|
228 |
+
"alpha": round(alpha, 4),
|
229 |
+
"beta": round(beta, 4),
|
230 |
+
"crisil_rank": mf.crisil_rank,
|
231 |
+
"overall_score": round(overall_score, 4),
|
232 |
+
}
|
233 |
+
)
|
234 |
+
logger.info("Finished calculating scores.")
|
235 |
+
return sorted(self.mf_scores, key=lambda d: d["overall_score"], reverse=True)
|
core/middleware.py
ADDED
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import logging
|
2 |
+
import traceback
|
3 |
+
|
4 |
+
from django.http import JsonResponse
|
5 |
+
|
6 |
+
|
7 |
+
logger = logging.getLogger(__name__)
|
8 |
+
|
9 |
+
|
10 |
+
class ExceptionMiddleware:
|
11 |
+
"""
|
12 |
+
Middleware to catch exceptions and handle them with appropriate logging and JSON response.
|
13 |
+
"""
|
14 |
+
|
15 |
+
def __init__(self, get_response):
|
16 |
+
"""
|
17 |
+
Initializes the ExceptionMiddleware with the provided get_response function.
|
18 |
+
"""
|
19 |
+
self.get_response = get_response
|
20 |
+
|
21 |
+
def __call__(self, request):
|
22 |
+
"""
|
23 |
+
Process the request and call the next middleware or view function in the chain.
|
24 |
+
"""
|
25 |
+
response = self.get_response(request)
|
26 |
+
return response
|
27 |
+
|
28 |
+
def process_exception(self, request, exception):
|
29 |
+
"""
|
30 |
+
Called when a view function raises an exception.
|
31 |
+
"""
|
32 |
+
error_type = exception.__class__.__name__
|
33 |
+
error_message = exception.args
|
34 |
+
logger.info(f"Error Type: {error_type} | Error Message: {error_message}")
|
35 |
+
logger.debug("Request Details: %s", request.__dict__)
|
36 |
+
logger.exception(traceback.format_exc())
|
37 |
+
|
38 |
+
if isinstance(exception, KeyError):
|
39 |
+
status_code = 400
|
40 |
+
message = f"Please Add Valid Data For {error_message[0]}"
|
41 |
+
error = "BAD_REQUEST"
|
42 |
+
elif isinstance(exception, AttributeError):
|
43 |
+
status_code = 500
|
44 |
+
message = "Something Went Wrong. Please try again."
|
45 |
+
error = "SOMETHING_WENT_WRONG"
|
46 |
+
elif isinstance(exception, TypeError):
|
47 |
+
status_code = 500
|
48 |
+
message = "Something Went Wrong. Please try again."
|
49 |
+
error = "SOMETHING_WENT_WRONG"
|
50 |
+
else:
|
51 |
+
status_code = 500
|
52 |
+
message = "Something Went Wrong. Please try again."
|
53 |
+
error = "SOMETHING_WENT_WRONG"
|
54 |
+
|
55 |
+
return JsonResponse({"message": message, "error": error}, status=status_code)
|
core/migrations/0001_initial.py
ADDED
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Generated by Django 4.2 on 2023-12-05 10:58
|
2 |
+
|
3 |
+
from django.db import migrations, models
|
4 |
+
import uuid
|
5 |
+
|
6 |
+
|
7 |
+
class Migration(migrations.Migration):
|
8 |
+
|
9 |
+
initial = True
|
10 |
+
|
11 |
+
dependencies = []
|
12 |
+
|
13 |
+
operations = [
|
14 |
+
migrations.CreateModel(
|
15 |
+
name="MutualFund",
|
16 |
+
fields=[
|
17 |
+
("created_at", models.DateTimeField(auto_now_add=True, null=True)),
|
18 |
+
("updated_at", models.DateTimeField(auto_now=True, null=True)),
|
19 |
+
(
|
20 |
+
"id",
|
21 |
+
models.UUIDField(
|
22 |
+
default=uuid.uuid4,
|
23 |
+
editable=False,
|
24 |
+
primary_key=True,
|
25 |
+
serialize=False,
|
26 |
+
),
|
27 |
+
),
|
28 |
+
("fund_house", models.CharField(max_length=200)),
|
29 |
+
(
|
30 |
+
"isin_number",
|
31 |
+
models.CharField(max_length=50, null=True, unique=True),
|
32 |
+
),
|
33 |
+
("security_id", models.CharField(max_length=50, unique=True)),
|
34 |
+
("data", models.JSONField(null=True)),
|
35 |
+
("rank", models.IntegerField(null=True, unique=True)),
|
36 |
+
],
|
37 |
+
options={
|
38 |
+
"abstract": False,
|
39 |
+
},
|
40 |
+
),
|
41 |
+
]
|
core/migrations/0002_rename_fund_house_mutualfund_fund_name.py
ADDED
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Generated by Django 4.2 on 2023-12-06 05:44
|
2 |
+
|
3 |
+
from django.db import migrations
|
4 |
+
|
5 |
+
|
6 |
+
class Migration(migrations.Migration):
|
7 |
+
|
8 |
+
dependencies = [
|
9 |
+
("core", "0001_initial"),
|
10 |
+
]
|
11 |
+
|
12 |
+
operations = [
|
13 |
+
migrations.RenameField(
|
14 |
+
model_name="mutualfund",
|
15 |
+
old_name="fund_house",
|
16 |
+
new_name="fund_name",
|
17 |
+
),
|
18 |
+
]
|
core/migrations/0002_stock.py
ADDED
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Generated by Django 4.2 on 2023-12-06 13:59
|
2 |
+
|
3 |
+
from django.db import migrations, models
|
4 |
+
import uuid
|
5 |
+
|
6 |
+
|
7 |
+
class Migration(migrations.Migration):
|
8 |
+
|
9 |
+
dependencies = [
|
10 |
+
("core", "0001_initial"),
|
11 |
+
]
|
12 |
+
|
13 |
+
operations = [
|
14 |
+
migrations.CreateModel(
|
15 |
+
name="Stock",
|
16 |
+
fields=[
|
17 |
+
("created_at", models.DateTimeField(auto_now_add=True, null=True)),
|
18 |
+
("updated_at", models.DateTimeField(auto_now=True, null=True)),
|
19 |
+
(
|
20 |
+
"id",
|
21 |
+
models.UUIDField(
|
22 |
+
default=uuid.uuid4,
|
23 |
+
editable=False,
|
24 |
+
primary_key=True,
|
25 |
+
serialize=False,
|
26 |
+
),
|
27 |
+
),
|
28 |
+
("name", models.CharField(max_length=200)),
|
29 |
+
("ltp", models.CharField(max_length=50, null=True)),
|
30 |
+
("percentage_change", models.CharField(max_length=50, null=True)),
|
31 |
+
("price_change", models.CharField(max_length=50, null=True)),
|
32 |
+
("link", models.URLField(max_length=50, null=True)),
|
33 |
+
("volume", models.CharField(max_length=50, null=True)),
|
34 |
+
("data", models.JSONField(null=True)),
|
35 |
+
("rank", models.IntegerField(null=True, unique=True)),
|
36 |
+
],
|
37 |
+
options={
|
38 |
+
"abstract": False,
|
39 |
+
},
|
40 |
+
),
|
41 |
+
]
|
core/migrations/0003_alter_mutualfund_fund_name.py
ADDED
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Generated by Django 4.2 on 2023-12-12 11:29
|
2 |
+
|
3 |
+
from django.db import migrations, models
|
4 |
+
|
5 |
+
|
6 |
+
class Migration(migrations.Migration):
|
7 |
+
|
8 |
+
dependencies = [
|
9 |
+
("core", "0002_rename_fund_house_mutualfund_fund_name"),
|
10 |
+
]
|
11 |
+
|
12 |
+
operations = [
|
13 |
+
migrations.AlterField(
|
14 |
+
model_name="mutualfund",
|
15 |
+
name="fund_name",
|
16 |
+
field=models.CharField(max_length=200, unique=True),
|
17 |
+
),
|
18 |
+
]
|
core/migrations/0003_alter_stock_link.py
ADDED
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Generated by Django 4.2 on 2023-12-06 14:07
|
2 |
+
|
3 |
+
from django.db import migrations, models
|
4 |
+
|
5 |
+
|
6 |
+
class Migration(migrations.Migration):
|
7 |
+
|
8 |
+
dependencies = [
|
9 |
+
("core", "0002_stock"),
|
10 |
+
]
|
11 |
+
|
12 |
+
operations = [
|
13 |
+
migrations.AlterField(
|
14 |
+
model_name="stock",
|
15 |
+
name="link",
|
16 |
+
field=models.URLField(null=True),
|
17 |
+
),
|
18 |
+
]
|
core/migrations/0004_stock_isin_number.py
ADDED
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Generated by Django 4.2 on 2023-12-07 06:26
|
2 |
+
|
3 |
+
from django.db import migrations, models
|
4 |
+
|
5 |
+
|
6 |
+
class Migration(migrations.Migration):
|
7 |
+
|
8 |
+
dependencies = [
|
9 |
+
("core", "0003_alter_stock_link"),
|
10 |
+
]
|
11 |
+
|
12 |
+
operations = [
|
13 |
+
migrations.AddField(
|
14 |
+
model_name="stock",
|
15 |
+
name="isin_number",
|
16 |
+
field=models.CharField(max_length=50, null=True, unique=True),
|
17 |
+
),
|
18 |
+
]
|
core/migrations/0005_merge_20231211_0610.py
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Generated by Django 4.2 on 2023-12-11 06:10
|
2 |
+
|
3 |
+
from django.db import migrations
|
4 |
+
|
5 |
+
|
6 |
+
class Migration(migrations.Migration):
|
7 |
+
|
8 |
+
dependencies = [
|
9 |
+
("core", "0002_rename_fund_house_mutualfund_fund_name"),
|
10 |
+
("core", "0004_stock_isin_number"),
|
11 |
+
]
|
12 |
+
|
13 |
+
operations = []
|
core/migrations/0006_mutualfund_crisil_rank.py
ADDED
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Generated by Django 4.2 on 2023-12-14 12:15
|
2 |
+
|
3 |
+
from django.db import migrations, models
|
4 |
+
|
5 |
+
|
6 |
+
class Migration(migrations.Migration):
|
7 |
+
|
8 |
+
dependencies = [
|
9 |
+
("core", "0005_merge_20231211_0610"),
|
10 |
+
]
|
11 |
+
|
12 |
+
operations = [
|
13 |
+
migrations.AddField(
|
14 |
+
model_name="mutualfund",
|
15 |
+
name="crisil_rank",
|
16 |
+
field=models.IntegerField(null=True),
|
17 |
+
)
|
18 |
+
]
|
core/migrations/0007_merge_20231214_1924.py
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Generated by Django 4.2 on 2023-12-14 19:24
|
2 |
+
|
3 |
+
from django.db import migrations
|
4 |
+
|
5 |
+
|
6 |
+
class Migration(migrations.Migration):
|
7 |
+
|
8 |
+
dependencies = [
|
9 |
+
("core", "0003_alter_mutualfund_fund_name"),
|
10 |
+
("core", "0006_mutualfund_crisil_rank"),
|
11 |
+
]
|
12 |
+
|
13 |
+
operations = []
|
core/migrations/0008_mutualfund_aum.py
ADDED
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Generated by Django 4.2 on 2023-12-14 19:24
|
2 |
+
|
3 |
+
from django.db import migrations, models
|
4 |
+
|
5 |
+
|
6 |
+
class Migration(migrations.Migration):
|
7 |
+
|
8 |
+
dependencies = [
|
9 |
+
("core", "0007_merge_20231214_1924"),
|
10 |
+
]
|
11 |
+
|
12 |
+
operations = [
|
13 |
+
migrations.AddField(
|
14 |
+
model_name="mutualfund",
|
15 |
+
name="aum",
|
16 |
+
field=models.FloatField(null=True),
|
17 |
+
),
|
18 |
+
]
|
core/migrations/0009_mutualfund_expense_ratio_mutualfund_return_m12_and_more.py
ADDED
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Generated by Django 4.2 on 2024-01-09 12:19
|
2 |
+
|
3 |
+
from django.db import migrations, models
|
4 |
+
import django.db.models.deletion
|
5 |
+
import uuid
|
6 |
+
|
7 |
+
|
8 |
+
class Migration(migrations.Migration):
|
9 |
+
|
10 |
+
dependencies = [
|
11 |
+
("core", "0008_mutualfund_aum"),
|
12 |
+
]
|
13 |
+
|
14 |
+
operations = [
|
15 |
+
migrations.AddField(
|
16 |
+
model_name="mutualfund",
|
17 |
+
name="expense_ratio",
|
18 |
+
field=models.FloatField(blank=True, null=True),
|
19 |
+
),
|
20 |
+
migrations.AddField(
|
21 |
+
model_name="mutualfund",
|
22 |
+
name="return_m12",
|
23 |
+
field=models.FloatField(blank=True, null=True),
|
24 |
+
),
|
25 |
+
migrations.CreateModel(
|
26 |
+
name="MFVolatility",
|
27 |
+
fields=[
|
28 |
+
(
|
29 |
+
"id",
|
30 |
+
models.UUIDField(
|
31 |
+
default=uuid.uuid4,
|
32 |
+
editable=False,
|
33 |
+
primary_key=True,
|
34 |
+
serialize=False,
|
35 |
+
),
|
36 |
+
),
|
37 |
+
(
|
38 |
+
"year",
|
39 |
+
models.CharField(
|
40 |
+
choices=[
|
41 |
+
(1, "for1Year"),
|
42 |
+
(3, "for3Year"),
|
43 |
+
(5, "for5Year"),
|
44 |
+
(10, "for10Year"),
|
45 |
+
(15, "for15Year"),
|
46 |
+
],
|
47 |
+
max_length=100,
|
48 |
+
),
|
49 |
+
),
|
50 |
+
("alpha", models.FloatField(blank=True, null=True)),
|
51 |
+
("beta", models.FloatField(blank=True, null=True)),
|
52 |
+
("sharpe_ratio", models.FloatField(blank=True, null=True)),
|
53 |
+
("standard_deviation", models.FloatField(blank=True, null=True)),
|
54 |
+
(
|
55 |
+
"mutual_fund",
|
56 |
+
models.ForeignKey(
|
57 |
+
on_delete=django.db.models.deletion.CASCADE,
|
58 |
+
to="core.mutualfund",
|
59 |
+
),
|
60 |
+
),
|
61 |
+
],
|
62 |
+
),
|
63 |
+
migrations.CreateModel(
|
64 |
+
name="MFHoldings",
|
65 |
+
fields=[
|
66 |
+
(
|
67 |
+
"id",
|
68 |
+
models.UUIDField(
|
69 |
+
default=uuid.uuid4,
|
70 |
+
editable=False,
|
71 |
+
primary_key=True,
|
72 |
+
serialize=False,
|
73 |
+
),
|
74 |
+
),
|
75 |
+
("isin_number", models.CharField(max_length=20)),
|
76 |
+
("security_id", models.CharField(max_length=20)),
|
77 |
+
("sector", models.CharField(max_length=50)),
|
78 |
+
("country", models.CharField(max_length=50)),
|
79 |
+
("currency", models.CharField(max_length=20)),
|
80 |
+
("weighting", models.FloatField(blank=True, null=True)),
|
81 |
+
("sector_code", models.CharField(max_length=20)),
|
82 |
+
("holding_type", models.CharField(max_length=20)),
|
83 |
+
("market_value", models.FloatField(blank=True, null=True)),
|
84 |
+
(
|
85 |
+
"stock_rating",
|
86 |
+
models.CharField(blank=True, max_length=10, null=True),
|
87 |
+
),
|
88 |
+
("total_assets", models.FloatField(blank=True, null=True)),
|
89 |
+
("currency_name", models.CharField(max_length=50)),
|
90 |
+
("maturity_date", models.DateField(blank=True, null=True)),
|
91 |
+
("security_name", models.CharField(max_length=100)),
|
92 |
+
("security_type", models.CharField(max_length=10)),
|
93 |
+
("holding_type_id", models.CharField(max_length=1)),
|
94 |
+
("number_of_shares", models.FloatField(blank=True, null=True)),
|
95 |
+
("one_year_return", models.FloatField(blank=True, null=True)),
|
96 |
+
("nav", models.FloatField(blank=True, null=True)),
|
97 |
+
(
|
98 |
+
"mutual_fund",
|
99 |
+
models.ForeignKey(
|
100 |
+
on_delete=django.db.models.deletion.CASCADE,
|
101 |
+
to="core.mutualfund",
|
102 |
+
),
|
103 |
+
),
|
104 |
+
],
|
105 |
+
),
|
106 |
+
]
|
core/migrations/0010_remove_mfholdings_nav_mutualfund_nav.py
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Generated by Django 4.2 on 2024-01-09 12:24
|
2 |
+
|
3 |
+
from django.db import migrations, models
|
4 |
+
|
5 |
+
|
6 |
+
class Migration(migrations.Migration):
|
7 |
+
|
8 |
+
dependencies = [
|
9 |
+
("core", "0009_mutualfund_expense_ratio_mutualfund_return_m12_and_more"),
|
10 |
+
]
|
11 |
+
|
12 |
+
operations = [
|
13 |
+
migrations.RemoveField(
|
14 |
+
model_name="mfholdings",
|
15 |
+
name="nav",
|
16 |
+
),
|
17 |
+
migrations.AddField(
|
18 |
+
model_name="mutualfund",
|
19 |
+
name="nav",
|
20 |
+
field=models.FloatField(blank=True, null=True),
|
21 |
+
),
|
22 |
+
]
|
core/migrations/0011_remove_mfholdings_maturity_date_and_more.py
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Generated by Django 4.2 on 2024-01-09 12:49
|
2 |
+
|
3 |
+
from django.db import migrations, models
|
4 |
+
|
5 |
+
|
6 |
+
class Migration(migrations.Migration):
|
7 |
+
|
8 |
+
dependencies = [
|
9 |
+
("core", "0010_remove_mfholdings_nav_mutualfund_nav"),
|
10 |
+
]
|
11 |
+
|
12 |
+
operations = [
|
13 |
+
migrations.RemoveField(
|
14 |
+
model_name="mfholdings",
|
15 |
+
name="maturity_date",
|
16 |
+
),
|
17 |
+
migrations.AlterField(
|
18 |
+
model_name="mfholdings",
|
19 |
+
name="holding_type_id",
|
20 |
+
field=models.CharField(max_length=10),
|
21 |
+
),
|
22 |
+
]
|
core/migrations/0012_alter_mfholdings_isin_number.py
ADDED
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Generated by Django 4.2 on 2024-01-09 13:06
|
2 |
+
|
3 |
+
from django.db import migrations, models
|
4 |
+
|
5 |
+
|
6 |
+
class Migration(migrations.Migration):
|
7 |
+
|
8 |
+
dependencies = [
|
9 |
+
("core", "0011_remove_mfholdings_maturity_date_and_more"),
|
10 |
+
]
|
11 |
+
|
12 |
+
operations = [
|
13 |
+
migrations.AlterField(
|
14 |
+
model_name="mfholdings",
|
15 |
+
name="isin_number",
|
16 |
+
field=models.CharField(blank=True, max_length=20, null=True),
|
17 |
+
),
|
18 |
+
]
|
core/migrations/0013_alter_mfholdings_country_alter_mfholdings_currency_and_more.py
ADDED
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Generated by Django 4.2 on 2024-01-09 13:08
|
2 |
+
|
3 |
+
from django.db import migrations, models
|
4 |
+
|
5 |
+
|
6 |
+
class Migration(migrations.Migration):
|
7 |
+
|
8 |
+
dependencies = [
|
9 |
+
("core", "0012_alter_mfholdings_isin_number"),
|
10 |
+
]
|
11 |
+
|
12 |
+
operations = [
|
13 |
+
migrations.AlterField(
|
14 |
+
model_name="mfholdings",
|
15 |
+
name="country",
|
16 |
+
field=models.CharField(blank=True, max_length=50, null=True),
|
17 |
+
),
|
18 |
+
migrations.AlterField(
|
19 |
+
model_name="mfholdings",
|
20 |
+
name="currency",
|
21 |
+
field=models.CharField(blank=True, max_length=20, null=True),
|
22 |
+
),
|
23 |
+
migrations.AlterField(
|
24 |
+
model_name="mfholdings",
|
25 |
+
name="currency_name",
|
26 |
+
field=models.CharField(blank=True, max_length=50, null=True),
|
27 |
+
),
|
28 |
+
migrations.AlterField(
|
29 |
+
model_name="mfholdings",
|
30 |
+
name="holding_type",
|
31 |
+
field=models.CharField(blank=True, max_length=20, null=True),
|
32 |
+
),
|
33 |
+
migrations.AlterField(
|
34 |
+
model_name="mfholdings",
|
35 |
+
name="holding_type_id",
|
36 |
+
field=models.CharField(blank=True, max_length=10, null=True),
|
37 |
+
),
|
38 |
+
migrations.AlterField(
|
39 |
+
model_name="mfholdings",
|
40 |
+
name="sector",
|
41 |
+
field=models.CharField(blank=True, max_length=50, null=True),
|
42 |
+
),
|
43 |
+
migrations.AlterField(
|
44 |
+
model_name="mfholdings",
|
45 |
+
name="sector_code",
|
46 |
+
field=models.CharField(blank=True, max_length=20, null=True),
|
47 |
+
),
|
48 |
+
migrations.AlterField(
|
49 |
+
model_name="mfholdings",
|
50 |
+
name="security_id",
|
51 |
+
field=models.CharField(blank=True, max_length=20, null=True),
|
52 |
+
),
|
53 |
+
migrations.AlterField(
|
54 |
+
model_name="mfholdings",
|
55 |
+
name="security_name",
|
56 |
+
field=models.CharField(blank=True, max_length=100, null=True),
|
57 |
+
),
|
58 |
+
migrations.AlterField(
|
59 |
+
model_name="mfholdings",
|
60 |
+
name="security_type",
|
61 |
+
field=models.CharField(blank=True, max_length=10, null=True),
|
62 |
+
),
|
63 |
+
]
|
core/migrations/0014_alter_mfholdings_currency_and_more.py
ADDED
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Generated by Django 4.2 on 2024-01-09 13:09
|
2 |
+
|
3 |
+
from django.db import migrations, models
|
4 |
+
|
5 |
+
|
6 |
+
class Migration(migrations.Migration):
|
7 |
+
|
8 |
+
dependencies = [
|
9 |
+
("core", "0013_alter_mfholdings_country_alter_mfholdings_currency_and_more"),
|
10 |
+
]
|
11 |
+
|
12 |
+
operations = [
|
13 |
+
migrations.AlterField(
|
14 |
+
model_name="mfholdings",
|
15 |
+
name="currency",
|
16 |
+
field=models.CharField(blank=True, max_length=100, null=True),
|
17 |
+
),
|
18 |
+
migrations.AlterField(
|
19 |
+
model_name="mfholdings",
|
20 |
+
name="currency_name",
|
21 |
+
field=models.CharField(blank=True, max_length=150, null=True),
|
22 |
+
),
|
23 |
+
migrations.AlterField(
|
24 |
+
model_name="mfholdings",
|
25 |
+
name="holding_type",
|
26 |
+
field=models.CharField(blank=True, max_length=100, null=True),
|
27 |
+
),
|
28 |
+
migrations.AlterField(
|
29 |
+
model_name="mfholdings",
|
30 |
+
name="holding_type_id",
|
31 |
+
field=models.CharField(blank=True, max_length=100, null=True),
|
32 |
+
),
|
33 |
+
migrations.AlterField(
|
34 |
+
model_name="mfholdings",
|
35 |
+
name="sector_code",
|
36 |
+
field=models.CharField(blank=True, max_length=100, null=True),
|
37 |
+
),
|
38 |
+
migrations.AlterField(
|
39 |
+
model_name="mfholdings",
|
40 |
+
name="security_type",
|
41 |
+
field=models.CharField(blank=True, max_length=100, null=True),
|
42 |
+
),
|
43 |
+
migrations.AlterField(
|
44 |
+
model_name="mfholdings",
|
45 |
+
name="stock_rating",
|
46 |
+
field=models.CharField(blank=True, max_length=100, null=True),
|
47 |
+
),
|
48 |
+
]
|
core/migrations/0015_rename_security_name_mfholdings_holding_name_and_more.py
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Generated by Django 4.2 on 2024-01-09 14:10
|
2 |
+
|
3 |
+
from django.db import migrations
|
4 |
+
|
5 |
+
|
6 |
+
class Migration(migrations.Migration):
|
7 |
+
|
8 |
+
dependencies = [
|
9 |
+
("core", "0014_alter_mfholdings_currency_and_more"),
|
10 |
+
]
|
11 |
+
|
12 |
+
operations = [
|
13 |
+
migrations.RenameField(
|
14 |
+
model_name="mfholdings",
|
15 |
+
old_name="security_name",
|
16 |
+
new_name="holding_name",
|
17 |
+
),
|
18 |
+
migrations.RemoveField(
|
19 |
+
model_name="mfholdings",
|
20 |
+
name="security_type",
|
21 |
+
),
|
22 |
+
]
|
core/migrations/__init__.py
ADDED
File without changes
|
core/models.py
ADDED
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import uuid
|
2 |
+
from django.db import models, connection
|
3 |
+
|
4 |
+
|
5 |
+
class BaseModel(models.Model):
|
6 |
+
|
7 |
+
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
8 |
+
created_at = models.DateTimeField(auto_now_add=True, null=True, blank=True)
|
9 |
+
updated_at = models.DateTimeField(auto_now=True, null=True, blank=True)
|
10 |
+
|
11 |
+
class Meta:
|
12 |
+
abstract = True
|
13 |
+
|
14 |
+
|
15 |
+
class MutualFund(BaseModel):
|
16 |
+
"""
|
17 |
+
This model will store the mutual fund data
|
18 |
+
"""
|
19 |
+
|
20 |
+
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
21 |
+
fund_name = models.CharField(max_length=200, unique=True)
|
22 |
+
isin_number = models.CharField(max_length=50, unique=True, null=True)
|
23 |
+
security_id = models.CharField(max_length=50, unique=True)
|
24 |
+
data = models.JSONField(null=True)
|
25 |
+
rank = models.IntegerField(unique=True, null=True)
|
26 |
+
crisil_rank = models.IntegerField(null=True)
|
27 |
+
aum = models.FloatField(null=True)
|
28 |
+
expense_ratio = models.FloatField(null=True, blank=True)
|
29 |
+
return_m12 = models.FloatField(null=True, blank=True)
|
30 |
+
nav = models.FloatField(null=True, blank=True)
|
31 |
+
|
32 |
+
@staticmethod
|
33 |
+
def execute_raw_sql_query(sql_query):
|
34 |
+
with connection.cursor() as cursor:
|
35 |
+
cursor.execute(sql_query)
|
36 |
+
columns = [col[0] for col in cursor.description]
|
37 |
+
results = [dict(zip(columns, row)) for row in cursor.fetchall()]
|
38 |
+
|
39 |
+
return results
|
40 |
+
|
41 |
+
@classmethod
|
42 |
+
def execute_query(cls, query):
|
43 |
+
try:
|
44 |
+
return cls.execute_raw_sql_query(query)
|
45 |
+
except Exception as e:
|
46 |
+
return []
|
47 |
+
|
48 |
+
class Stock(BaseModel):
|
49 |
+
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
50 |
+
name = models.CharField(max_length=200)
|
51 |
+
ltp = models.CharField(max_length=50, null=True)
|
52 |
+
percentage_change = models.CharField(max_length=50, null=True)
|
53 |
+
price_change = models.CharField(max_length=50, null=True)
|
54 |
+
link = models.URLField(max_length=200, null=True)
|
55 |
+
volume = models.CharField(max_length=50, null=True)
|
56 |
+
data = models.JSONField(null=True)
|
57 |
+
isin_number = models.CharField(max_length=50, unique=True, null=True)
|
58 |
+
rank = models.IntegerField(unique=True, null=True)
|
59 |
+
|
60 |
+
|
61 |
+
class MFHoldings(models.Model):
|
62 |
+
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
63 |
+
isin_number = models.CharField(max_length=20, null=True, blank=True)
|
64 |
+
security_id = models.CharField(max_length=20, null=True, blank=True)
|
65 |
+
sector = models.CharField(max_length=50, null=True, blank=True)
|
66 |
+
country = models.CharField(max_length=50, null=True, blank=True)
|
67 |
+
currency = models.CharField(max_length=100, null=True, blank=True)
|
68 |
+
weighting = models.FloatField(null=True, blank=True)
|
69 |
+
sector_code = models.CharField(max_length=100, null=True, blank=True)
|
70 |
+
holding_type = models.CharField(max_length=100, null=True, blank=True)
|
71 |
+
market_value = models.FloatField(null=True, blank=True)
|
72 |
+
stock_rating = models.CharField(max_length=100, null=True, blank=True)
|
73 |
+
total_assets = models.FloatField(null=True, blank=True)
|
74 |
+
currency_name = models.CharField(max_length=150, null=True, blank=True)
|
75 |
+
holding_name = models.CharField(max_length=100, null=True, blank=True)
|
76 |
+
holding_type = models.CharField(max_length=100, null=True, blank=True)
|
77 |
+
holding_type_id = models.CharField(max_length=100, null=True, blank=True)
|
78 |
+
number_of_shares = models.FloatField(null=True, blank=True)
|
79 |
+
one_year_return = models.FloatField(null=True, blank=True)
|
80 |
+
mutual_fund = models.ForeignKey(MutualFund, on_delete=models.CASCADE)
|
81 |
+
|
82 |
+
def __str__(self):
|
83 |
+
return f"{self.ticker} - {self.securityName}"
|
84 |
+
|
85 |
+
|
86 |
+
class MFVolatility(models.Model):
|
87 |
+
VOLATILITY_CHOICES = (
|
88 |
+
(1, "for1Year"),
|
89 |
+
(3, "for3Year"),
|
90 |
+
(5, "for5Year"),
|
91 |
+
(10, "for10Year"),
|
92 |
+
(15, "for15Year"),
|
93 |
+
)
|
94 |
+
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
95 |
+
mutual_fund = models.ForeignKey(MutualFund, on_delete=models.CASCADE)
|
96 |
+
year = models.CharField(max_length=100, choices=VOLATILITY_CHOICES)
|
97 |
+
alpha = models.FloatField(null=True, blank=True)
|
98 |
+
beta = models.FloatField(null=True, blank=True)
|
99 |
+
sharpe_ratio = models.FloatField(null=True, blank=True)
|
100 |
+
standard_deviation = models.FloatField(null=True, blank=True)
|
core/test_models.py
ADDED
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from django.test import TestCase
|
2 |
+
from core.models import MutualFund
|
3 |
+
|
4 |
+
|
5 |
+
class TestMutualFund(TestCase):
|
6 |
+
def test_model_creation(self):
|
7 |
+
# Create a new MutualFund instance
|
8 |
+
mutual_fund1 = MutualFund(
|
9 |
+
fund_name="Test Fund 1",
|
10 |
+
isin_number="123456789012",
|
11 |
+
security_id="MST01234",
|
12 |
+
data={
|
13 |
+
"details": {
|
14 |
+
"legalName": "Test Fund 1",
|
15 |
+
"isin": "123456789012",
|
16 |
+
"secId": "MST01234",
|
17 |
+
}
|
18 |
+
},
|
19 |
+
)
|
20 |
+
|
21 |
+
# Save the MutualFund instance to the database
|
22 |
+
mutual_fund1.save()
|
23 |
+
|
24 |
+
# Check if the MutualFund instance was saved successfully
|
25 |
+
self.assertEqual(MutualFund.objects.count(), 1)
|
26 |
+
|
27 |
+
mutual_fund2 = MutualFund(
|
28 |
+
fund_name="Test Fund 2",
|
29 |
+
isin_number="9876543210",
|
30 |
+
security_id="MST56789",
|
31 |
+
data={
|
32 |
+
"details": {
|
33 |
+
"legalName": "Test Fund 2",
|
34 |
+
"isin": "9876543210",
|
35 |
+
"secId": "MST56789",
|
36 |
+
}
|
37 |
+
},
|
38 |
+
)
|
39 |
+
mutual_fund2.save()
|
40 |
+
|
41 |
+
self.assertNotEqual(mutual_fund1.id, mutual_fund2.id)
|
42 |
+
self.assertEqual(MutualFund.objects.count(), 2)
|
core/tests/__init__.py
ADDED
File without changes
|
core/tests/data.py
ADDED
@@ -0,0 +1,339 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
test_data = {
|
2 |
+
1: {
|
3 |
+
"quotes": {"expenseRatio": 0.023399999999999997, "lastTurnoverRatio": 0.1568},
|
4 |
+
"holdings": {
|
5 |
+
"equityHoldingPage": {
|
6 |
+
"pageSize": 100,
|
7 |
+
"totalPage": 1,
|
8 |
+
"pageNumber": 1,
|
9 |
+
"holdingList": [
|
10 |
+
{
|
11 |
+
"isin": "INE280A01028",
|
12 |
+
"weighting": 9.3,
|
13 |
+
},
|
14 |
+
{
|
15 |
+
"isin": "INE002A01018",
|
16 |
+
"weighting": 7.1,
|
17 |
+
},
|
18 |
+
{"isin": "INE090A01021", "weighting": 2.4},
|
19 |
+
{"isin": "INE040A01034", "weighting": 1.1},
|
20 |
+
],
|
21 |
+
},
|
22 |
+
},
|
23 |
+
"list_info": {"isin": "MF123"},
|
24 |
+
"risk_measures": {
|
25 |
+
"cur": "INR",
|
26 |
+
"fundName": "Testing fund 2",
|
27 |
+
"indexName": "Morningstar India GR INR",
|
28 |
+
"categoryName": "Large-Cap",
|
29 |
+
"fundRiskVolatility": {
|
30 |
+
"endDate": "2023-11-30T06:00:00.000",
|
31 |
+
"for1Year": {
|
32 |
+
"beta": 1.002,
|
33 |
+
"alpha": 4.464,
|
34 |
+
"rSquared": 98.8,
|
35 |
+
"sharpeRatio": 0.877,
|
36 |
+
"standardDeviation": 11.355,
|
37 |
+
},
|
38 |
+
"for3Year": {
|
39 |
+
"beta": None,
|
40 |
+
"alpha": None,
|
41 |
+
"rSquared": None,
|
42 |
+
"sharpeRatio": None,
|
43 |
+
"standardDeviation": None,
|
44 |
+
},
|
45 |
+
"for5Year": {
|
46 |
+
"beta": None,
|
47 |
+
"alpha": None,
|
48 |
+
"rSquared": None,
|
49 |
+
"sharpeRatio": None,
|
50 |
+
"standardDeviation": None,
|
51 |
+
},
|
52 |
+
"for10Year": {
|
53 |
+
"beta": None,
|
54 |
+
"alpha": None,
|
55 |
+
"rSquared": None,
|
56 |
+
"sharpeRatio": None,
|
57 |
+
"standardDeviation": None,
|
58 |
+
},
|
59 |
+
"for15Year": {
|
60 |
+
"beta": None,
|
61 |
+
"alpha": None,
|
62 |
+
"rSquared": None,
|
63 |
+
"sharpeRatio": None,
|
64 |
+
"standardDeviation": None,
|
65 |
+
},
|
66 |
+
"bestFitIndexName": None,
|
67 |
+
"forLongestTenure": None,
|
68 |
+
"bestFitBetaFor3Year": None,
|
69 |
+
"primaryIndexNameNew": "S&P BSE 100 India TR INR",
|
70 |
+
"bestFitAlphaFor3Year": None,
|
71 |
+
"bestFitRSquaredFor3Year": None,
|
72 |
+
},
|
73 |
+
},
|
74 |
+
},
|
75 |
+
2: {
|
76 |
+
"quotes": {"expenseRatio": 0.023399999999999997, "lastTurnoverRatio": 0.5158},
|
77 |
+
"holdings": {
|
78 |
+
"equityHoldingPage": {
|
79 |
+
"pageSize": 100,
|
80 |
+
"totalPage": 1,
|
81 |
+
"pageNumber": 1,
|
82 |
+
"holdingList": [
|
83 |
+
{"isin": "INE040A01034", "weighting": 7.6},
|
84 |
+
{"isin": "INE154A01025", "weighting": 1.1},
|
85 |
+
{"isin": "INE018A01030", "weighting": 3.2},
|
86 |
+
{"isin": "INE154A01025", "weighting": 3.1},
|
87 |
+
],
|
88 |
+
},
|
89 |
+
},
|
90 |
+
"list_info": {"isin": "MF123"},
|
91 |
+
"risk_measures": {
|
92 |
+
"cur": "INR",
|
93 |
+
"fundName": "Testing fund 2",
|
94 |
+
"indexName": "Morningstar India GR INR",
|
95 |
+
"categoryName": "Large-Cap",
|
96 |
+
"fundRiskVolatility": {
|
97 |
+
"endDate": "2023-11-30T06:00:00.000",
|
98 |
+
"for1Year": {
|
99 |
+
"beta": 0.994,
|
100 |
+
"alpha": 0.337,
|
101 |
+
"rSquared": 98.8,
|
102 |
+
"sharpeRatio": 0.341,
|
103 |
+
"standardDeviation": 11.355,
|
104 |
+
},
|
105 |
+
"for3Year": {
|
106 |
+
"beta": None,
|
107 |
+
"alpha": None,
|
108 |
+
"rSquared": None,
|
109 |
+
"sharpeRatio": None,
|
110 |
+
"standardDeviation": None,
|
111 |
+
},
|
112 |
+
"for5Year": {
|
113 |
+
"beta": None,
|
114 |
+
"alpha": None,
|
115 |
+
"rSquared": None,
|
116 |
+
"sharpeRatio": None,
|
117 |
+
"standardDeviation": None,
|
118 |
+
},
|
119 |
+
"for10Year": {
|
120 |
+
"beta": None,
|
121 |
+
"alpha": None,
|
122 |
+
"rSquared": None,
|
123 |
+
"sharpeRatio": None,
|
124 |
+
"standardDeviation": None,
|
125 |
+
},
|
126 |
+
"for15Year": {
|
127 |
+
"beta": None,
|
128 |
+
"alpha": None,
|
129 |
+
"rSquared": None,
|
130 |
+
"sharpeRatio": None,
|
131 |
+
"standardDeviation": None,
|
132 |
+
},
|
133 |
+
"bestFitIndexName": None,
|
134 |
+
"forLongestTenure": None,
|
135 |
+
"bestFitBetaFor3Year": None,
|
136 |
+
"primaryIndexNameNew": "S&P BSE 100 India TR INR",
|
137 |
+
"bestFitAlphaFor3Year": None,
|
138 |
+
"bestFitRSquaredFor3Year": None,
|
139 |
+
},
|
140 |
+
},
|
141 |
+
},
|
142 |
+
3: {
|
143 |
+
"quotes": {"expenseRatio": 0.0106, "lastTurnoverRatio": 0.3109},
|
144 |
+
"holdings": {
|
145 |
+
"equityHoldingPage": {
|
146 |
+
"pageSize": 100,
|
147 |
+
"totalPage": 1,
|
148 |
+
"pageNumber": 1,
|
149 |
+
"holdingList": [
|
150 |
+
{"isin": "INE280A01028", "weighting": 11.2},
|
151 |
+
{"isin": "INE090A01021", "weighting": 7.1},
|
152 |
+
{"isin": "INE040A01034", "weighting": 2.4},
|
153 |
+
],
|
154 |
+
},
|
155 |
+
},
|
156 |
+
"list_info": {"isin": "MF123"},
|
157 |
+
"risk_measures": {
|
158 |
+
"cur": "INR",
|
159 |
+
"fundName": "Testing fund 2",
|
160 |
+
"indexName": "Morningstar India GR INR",
|
161 |
+
"categoryName": "Large-Cap",
|
162 |
+
"fundRiskVolatility": {
|
163 |
+
"endDate": "2023-11-30T06:00:00.000",
|
164 |
+
"for1Year": {
|
165 |
+
"beta": 0.954,
|
166 |
+
"alpha": -0.645,
|
167 |
+
"rSquared": 98.8,
|
168 |
+
"sharpeRatio": 0.251,
|
169 |
+
"standardDeviation": 11.355,
|
170 |
+
},
|
171 |
+
"for3Year": {
|
172 |
+
"beta": None,
|
173 |
+
"alpha": None,
|
174 |
+
"rSquared": None,
|
175 |
+
"sharpeRatio": None,
|
176 |
+
"standardDeviation": None,
|
177 |
+
},
|
178 |
+
"for5Year": {
|
179 |
+
"beta": None,
|
180 |
+
"alpha": None,
|
181 |
+
"rSquared": None,
|
182 |
+
"sharpeRatio": None,
|
183 |
+
"standardDeviation": None,
|
184 |
+
},
|
185 |
+
"for10Year": {
|
186 |
+
"beta": None,
|
187 |
+
"alpha": None,
|
188 |
+
"rSquared": None,
|
189 |
+
"sharpeRatio": None,
|
190 |
+
"standardDeviation": None,
|
191 |
+
},
|
192 |
+
"for15Year": {
|
193 |
+
"beta": None,
|
194 |
+
"alpha": None,
|
195 |
+
"rSquared": None,
|
196 |
+
"sharpeRatio": None,
|
197 |
+
"standardDeviation": None,
|
198 |
+
},
|
199 |
+
"bestFitIndexName": None,
|
200 |
+
"forLongestTenure": None,
|
201 |
+
"bestFitBetaFor3Year": None,
|
202 |
+
"primaryIndexNameNew": "S&P BSE 100 India TR INR",
|
203 |
+
"bestFitAlphaFor3Year": None,
|
204 |
+
"bestFitRSquaredFor3Year": None,
|
205 |
+
},
|
206 |
+
},
|
207 |
+
},
|
208 |
+
4: {
|
209 |
+
"quotes": {"expenseRatio": 0.0158, "lastTurnoverRatio": 0.5451},
|
210 |
+
"holdings": {
|
211 |
+
"equityHoldingPage": {
|
212 |
+
"pageSize": 100,
|
213 |
+
"totalPage": 1,
|
214 |
+
"pageNumber": 1,
|
215 |
+
"holdingList": [
|
216 |
+
{"isin": "INE280A01028", "weighting": 10.2},
|
217 |
+
{"isin": "INE002A01018", "weighting": 9.2},
|
218 |
+
],
|
219 |
+
},
|
220 |
+
},
|
221 |
+
"list_info": {"isin": "MF123"},
|
222 |
+
"risk_measures": {
|
223 |
+
"cur": "INR",
|
224 |
+
"fundName": "Testing fund 2",
|
225 |
+
"indexName": "Morningstar India GR INR",
|
226 |
+
"categoryName": "Large-Cap",
|
227 |
+
"fundRiskVolatility": {
|
228 |
+
"endDate": "2023-11-30T06:00:00.000",
|
229 |
+
"for1Year": {
|
230 |
+
"beta": 1.137,
|
231 |
+
"alpha": 5.675,
|
232 |
+
"rSquared": 98.8,
|
233 |
+
"sharpeRatio": 0.735,
|
234 |
+
"standardDeviation": 11.355,
|
235 |
+
},
|
236 |
+
"for3Year": {
|
237 |
+
"beta": None,
|
238 |
+
"alpha": None,
|
239 |
+
"rSquared": None,
|
240 |
+
"sharpeRatio": None,
|
241 |
+
"standardDeviation": None,
|
242 |
+
},
|
243 |
+
"for5Year": {
|
244 |
+
"beta": None,
|
245 |
+
"alpha": None,
|
246 |
+
"rSquared": None,
|
247 |
+
"sharpeRatio": None,
|
248 |
+
"standardDeviation": None,
|
249 |
+
},
|
250 |
+
"for10Year": {
|
251 |
+
"beta": None,
|
252 |
+
"alpha": None,
|
253 |
+
"rSquared": None,
|
254 |
+
"sharpeRatio": None,
|
255 |
+
"standardDeviation": None,
|
256 |
+
},
|
257 |
+
"for15Year": {
|
258 |
+
"beta": None,
|
259 |
+
"alpha": None,
|
260 |
+
"rSquared": None,
|
261 |
+
"sharpeRatio": None,
|
262 |
+
"standardDeviation": None,
|
263 |
+
},
|
264 |
+
"bestFitIndexName": None,
|
265 |
+
"forLongestTenure": None,
|
266 |
+
"bestFitBetaFor3Year": None,
|
267 |
+
"primaryIndexNameNew": "S&P BSE 100 India TR INR",
|
268 |
+
"bestFitAlphaFor3Year": None,
|
269 |
+
"bestFitRSquaredFor3Year": None,
|
270 |
+
},
|
271 |
+
},
|
272 |
+
},
|
273 |
+
5: {
|
274 |
+
"quotes": {"expenseRatio": 0.0216, "lastTurnoverRatio": 0.2025},
|
275 |
+
"holdings": {
|
276 |
+
"equityHoldingPage": {
|
277 |
+
"pageSize": 100,
|
278 |
+
"totalPage": 1,
|
279 |
+
"pageNumber": 1,
|
280 |
+
"holdingList": [
|
281 |
+
{"isin": "INE002A01018", "weighting": 13.2},
|
282 |
+
{"isin": "INE090A01021", "weighting": 7.4},
|
283 |
+
{"isin": "INE040A01034", "weighting": 3.4},
|
284 |
+
],
|
285 |
+
},
|
286 |
+
},
|
287 |
+
"list_info": {"isin": "MF123"},
|
288 |
+
"risk_measures": {
|
289 |
+
"cur": "INR",
|
290 |
+
"fundName": "Testing fund 2",
|
291 |
+
"indexName": "Morningstar India GR INR",
|
292 |
+
"categoryName": "Large-Cap",
|
293 |
+
"fundRiskVolatility": {
|
294 |
+
"endDate": "2023-11-30T06:00:00.000",
|
295 |
+
"for1Year": {
|
296 |
+
"beta": 1.041,
|
297 |
+
"alpha": 0.521,
|
298 |
+
"rSquared": 98.8,
|
299 |
+
"sharpeRatio": 0.354,
|
300 |
+
"standardDeviation": 11.355,
|
301 |
+
},
|
302 |
+
"for3Year": {
|
303 |
+
"beta": None,
|
304 |
+
"alpha": None,
|
305 |
+
"rSquared": None,
|
306 |
+
"sharpeRatio": None,
|
307 |
+
"standardDeviation": None,
|
308 |
+
},
|
309 |
+
"for5Year": {
|
310 |
+
"beta": None,
|
311 |
+
"alpha": None,
|
312 |
+
"rSquared": None,
|
313 |
+
"sharpeRatio": None,
|
314 |
+
"standardDeviation": None,
|
315 |
+
},
|
316 |
+
"for10Year": {
|
317 |
+
"beta": None,
|
318 |
+
"alpha": None,
|
319 |
+
"rSquared": None,
|
320 |
+
"sharpeRatio": None,
|
321 |
+
"standardDeviation": None,
|
322 |
+
},
|
323 |
+
"for15Year": {
|
324 |
+
"beta": None,
|
325 |
+
"alpha": None,
|
326 |
+
"rSquared": None,
|
327 |
+
"sharpeRatio": None,
|
328 |
+
"standardDeviation": None,
|
329 |
+
},
|
330 |
+
"bestFitIndexName": None,
|
331 |
+
"forLongestTenure": None,
|
332 |
+
"bestFitBetaFor3Year": None,
|
333 |
+
"primaryIndexNameNew": "S&P BSE 100 India TR INR",
|
334 |
+
"bestFitAlphaFor3Year": None,
|
335 |
+
"bestFitRSquaredFor3Year": None,
|
336 |
+
},
|
337 |
+
},
|
338 |
+
},
|
339 |
+
}
|
core/tests/test_scores.py
ADDED
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from django.test import TestCase
|
2 |
+
from core.models import MutualFund, Stock
|
3 |
+
from core.mfrating.score_calculator import MutualFundScorer, MFRating
|
4 |
+
from core.tests.data import test_data
|
5 |
+
|
6 |
+
|
7 |
+
class MutualFundScorerTestCase(TestCase):
|
8 |
+
"""
|
9 |
+
Test case for the MutualFundScorer class to test scores.
|
10 |
+
"""
|
11 |
+
|
12 |
+
def setUp(self):
|
13 |
+
self.stock_data = [
|
14 |
+
{"isin_number": "INE040A01034", "rank": 10},
|
15 |
+
{"isin_number": "INE090A01021", "rank": 21},
|
16 |
+
{"isin_number": "INE002A01018", "rank": 131},
|
17 |
+
{"isin_number": "INE154A01025", "rank": 99},
|
18 |
+
{"isin_number": "INE018A01030", "rank": 31},
|
19 |
+
{"isin_number": "INE280A01028", "rank": 2},
|
20 |
+
]
|
21 |
+
|
22 |
+
self.mutual_fund_data = [
|
23 |
+
{
|
24 |
+
"isin_number": "ISIN1",
|
25 |
+
"fund_name": "Testing Fund 1",
|
26 |
+
"rank": 1,
|
27 |
+
"aum": 837.3,
|
28 |
+
"crisil_rank": 4,
|
29 |
+
"security_id": "SEC1",
|
30 |
+
"data": test_data[1],
|
31 |
+
},
|
32 |
+
{
|
33 |
+
"isin_number": "ISIN2",
|
34 |
+
"fund_name": "Testing Fund 2",
|
35 |
+
"rank": 2,
|
36 |
+
"aum": 210.3,
|
37 |
+
"crisil_rank": 1,
|
38 |
+
"security_id": "SEC2",
|
39 |
+
"data": test_data[2],
|
40 |
+
},
|
41 |
+
{
|
42 |
+
"isin_number": "ISIN3",
|
43 |
+
"fund_name": "Testing Fund 3",
|
44 |
+
"rank": 3,
|
45 |
+
"aum": 639.3,
|
46 |
+
"crisil_rank": 3,
|
47 |
+
"security_id": "SEC3",
|
48 |
+
"data": test_data[3],
|
49 |
+
},
|
50 |
+
{
|
51 |
+
"isin_number": "ISIN4",
|
52 |
+
"fund_name": "Testing Fund 4",
|
53 |
+
"rank": 4,
|
54 |
+
"aum": 410.3,
|
55 |
+
"crisil_rank": 2,
|
56 |
+
"security_id": "SEC4",
|
57 |
+
"data": test_data[4],
|
58 |
+
},
|
59 |
+
{
|
60 |
+
"isin_number": "ISIN5",
|
61 |
+
"fund_name": "Testing Fund 5",
|
62 |
+
"rank": 5,
|
63 |
+
"aum": 1881.3,
|
64 |
+
"crisil_rank": 5,
|
65 |
+
"security_id": "SEC5",
|
66 |
+
"data": test_data[5],
|
67 |
+
},
|
68 |
+
]
|
69 |
+
|
70 |
+
self.create_stock_objects()
|
71 |
+
self.create_mutual_fund_objects()
|
72 |
+
self.mf_scorer = MutualFundScorer()
|
73 |
+
|
74 |
+
def create_stock_objects(self):
|
75 |
+
"""
|
76 |
+
Create stock objects using the predefined stock data.
|
77 |
+
"""
|
78 |
+
self.stock_objects = [Stock.objects.create(**data) for data in self.stock_data]
|
79 |
+
|
80 |
+
def create_mutual_fund_objects(self):
|
81 |
+
"""
|
82 |
+
Create mutual fund objects using the predefined mutual fund data.
|
83 |
+
"""
|
84 |
+
self.mutual_fund_objects = [
|
85 |
+
MutualFund.objects.create(**data) for data in self.mutual_fund_data
|
86 |
+
]
|
87 |
+
|
88 |
+
def test_get_scores_returns_sorted_list(self):
|
89 |
+
"""
|
90 |
+
Test whether the get_scores method returns a sorted list of scores.
|
91 |
+
"""
|
92 |
+
scores = self.mf_scorer.get_scores()
|
93 |
+
self.assertEqual(len(scores), 5)
|
94 |
+
self.assertEqual(
|
95 |
+
scores, sorted(scores, key=lambda x: x["overall_score"], reverse=True)
|
96 |
+
)
|
97 |
+
expected_scores = [0.4263, 0.3348, 0.2962, 0.2447, 0.2101]
|
98 |
+
for i, expected_score in enumerate(expected_scores):
|
99 |
+
self.assertAlmostEqual(
|
100 |
+
scores[i]["overall_score"], expected_score, delta=1e-4
|
101 |
+
)
|
core/text2sql/__init__.py
ADDED
File without changes
|
core/text2sql/eval_queries.py
ADDED
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
queries = [
|
2 |
+
{"Query Number": 1, "Complexity Level": "Simple", "Query Description": "Retrieve all mutual funds and their names",
|
3 |
+
"SQL Statement": "SELECT id, fund_name FROM core_mutualfund;"},
|
4 |
+
{"Query Number": 2, "Complexity Level": "Simple", "Query Description": "Get the total number of mutual funds",
|
5 |
+
"SQL Statement": "SELECT COUNT(*) FROM core_mutualfund;"},
|
6 |
+
{"Query Number": 3, "Complexity Level": "Simple", "Query Description": "List all unique ISIN numbers in the mutual fund holdings",
|
7 |
+
"SQL Statement": "SELECT DISTINCT isin_number FROM core_mfholdings;"},
|
8 |
+
{"Query Number": 4, "Complexity Level": "Simple", "Query Description":
|
9 |
+
"Find the mutual fund with the highest AUM (Assets Under Management)", "SQL Statement": "SELECT * FROM core_mutualfund ORDER BY aum DESC LIMIT 1;"},
|
10 |
+
{"Query Number": 5, "Complexity Level": "Simple", "Query Description": "Retrieve the top 5 mutual funds with the highest one-year return",
|
11 |
+
"SQL Statement": "SELECT * FROM core_mutualfund ORDER BY return_m12 DESC LIMIT 5;"},
|
12 |
+
{"Query Number": 6, "Complexity Level": "Medium", "Query Description": "List mutual funds with their holdings and respective sector codes",
|
13 |
+
"SQL Statement": "SELECT m.fund_name, h.sector, h.sector_code FROM core_mutualfund m JOIN core_mfholdings h ON m.id = h.mutual_fund_id;"},
|
14 |
+
{"Query Number": 7, "Complexity Level": "Medium", "Query Description": "Find the average expense ratio for all mutual funds",
|
15 |
+
"SQL Statement": "SELECT AVG(expense_ratio) FROM core_mutualfund;"},
|
16 |
+
{"Query Number": 8, "Complexity Level": "Medium", "Query Description": "Retrieve mutual funds with a specific country in their holdings",
|
17 |
+
"SQL Statement": "SELECT m.fund_name, h.country FROM core_mutualfund m JOIN core_mfholdings h ON m.id = h.mutual_fund_id WHERE h.country = 'USA';"},
|
18 |
+
{"Query Number": 9, "Complexity Level": "Medium", "Query Description":
|
19 |
+
"List mutual funds with volatility metrics (alpha, beta, sharpe_ratio)", "SQL Statement": "SELECT m.fund_name, v.alpha, v.beta, v.sharpe_ratio FROM core_mutualfund m JOIN core_mfvolatility v ON m.id = v.mutual_fund_id;"},
|
20 |
+
{"Query Number": 10, "Complexity Level": "Medium", "Query Description":
|
21 |
+
"Retrieve mutual funds with a NAV (Net Asset Value) greater than a specific value", "SQL Statement": "SELECT * FROM core_mutualfund WHERE nav > 100;"},
|
22 |
+
{"Query Number": 11, "Complexity Level": "High", "Query Description": "Find the mutual fund with the highest total market value of holdings",
|
23 |
+
"SQL Statement": "SELECT m.fund_name, MAX(h.market_value) AS max_market_value FROM core_mutualfund m JOIN core_mfholdings h ON m.id = h.mutual_fund_id;"},
|
24 |
+
{"Query Number": 12, "Complexity Level": "High", "Query Description": "List mutual funds with their average one-year return grouped by sector",
|
25 |
+
"SQL Statement": "SELECT h.sector, AVG(m.return_m12) AS avg_one_year_return FROM core_mutualfund m JOIN core_mfholdings h ON m.id = h.mutual_fund_id GROUP BY h.sector;"},
|
26 |
+
{"Query Number": 13, "Complexity Level": "High", "Query Description": "Retrieve mutual funds with a specific stock rating in their holdings",
|
27 |
+
"SQL Statement": "SELECT m.fund_name, h.stock_rating FROM core_mutualfund m JOIN core_mfholdings h ON m.id = h.mutual_fund_id WHERE h.stock_rating = 'A';"},
|
28 |
+
{"Query Number": 14, "Complexity Level": "High", "Query Description": "Find the mutual fund with the lowest standard deviation of volatility",
|
29 |
+
"SQL Statement": "SELECT m.fund_name, MIN(v.standard_deviation) AS min_standard_deviation FROM core_mutualfund m JOIN core_mfvolatility v ON m.id = v.mutual_fund_id;"},
|
30 |
+
{"Query Number": 15, "Complexity Level": "High", "Query Description": "List mutual funds with the highest number of shares in their holdings",
|
31 |
+
"SQL Statement": "SELECT m.fund_name, MAX(h.number_of_shares) AS max_number_of_shares FROM core_mutualfund m JOIN core_mfholdings h ON m.id = h.mutual_fund_id;"},
|
32 |
+
{"Query Number": 16, "Complexity Level": "More Complex", "Query Description": "Retrieve mutual funds and their holdings with a specific currency",
|
33 |
+
"SQL Statement": "SELECT m.fund_name, h.currency FROM core_mutualfund m JOIN core_mfholdings h ON m.id = h.mutual_fund_id WHERE h.currency = 'USD';"},
|
34 |
+
{"Query Number": 17, "Complexity Level": "More Complex", "Query Description": "Find the mutual fund with the highest total market value across all holdings",
|
35 |
+
"SQL Statement": "SELECT m.fund_name, SUM(h.market_value) AS total_market_value FROM core_mutualfund m JOIN core_mfholdings h ON m.id = h.mutual_fund_id GROUP BY m.fund_name ORDER BY total_market_value DESC LIMIT 1;"},
|
36 |
+
{"Query Number": 18, "Complexity Level": "More Complex", "Query Description": "List mutual funds with their average alpha and beta values for a specific year",
|
37 |
+
"SQL Statement": "SELECT m.fund_name, AVG(v.alpha) AS avg_alpha, AVG(v.beta) AS avg_beta FROM core_mutualfund m JOIN core_mfvolatility v ON m.id = v.mutual_fund_id WHERE v.year = '2023' GROUP BY m.fund_name;"},
|
38 |
+
{"Query Number": 19, "Complexity Level": "More Complex", "Query Description": "Retrieve mutual funds with a specific holding type and its market value",
|
39 |
+
"SQL Statement": "SELECT m.fund_name, h.holding_type, h.market_value FROM core_mutualfund m JOIN core_mfholdings h ON m.id = h.mutual_fund_id WHERE h.holding_type = 'Equity';"},
|
40 |
+
{"Query Number": 20, "Complexity Level": "More Complex", "Query Description": "List mutual funds with their rankings and corresponding CRISIL rankings",
|
41 |
+
"SQL Statement": "SELECT m.fund_name, m.rank, m.crisil_rank FROM core_mutualfund m;"},
|
42 |
+
{"Query Number": 21, "Complexity Level": "More Complex", "Query Description": "Find the mutual fund with the highest average one-year return across all years",
|
43 |
+
"SQL Statement": "SELECT m.fund_name, AVG(m.return_m12) AS avg_one_year_return FROM core_mutualfund m GROUP BY m.fund_name ORDER BY avg_one_year_return DESC LIMIT 1;"},
|
44 |
+
{"Query Number": 22, "Complexity Level": "More Complex", "Query Description": "Retrieve mutual funds with their top 3 holdings based on market value",
|
45 |
+
"SQL Statement": "SELECT m.fund_name, h.holding_name, h.market_value FROM core_mutualfund m JOIN core_mfholdings h ON m.id = h.mutual_fund_id ORDER BY h.market_value DESC LIMIT 3;"},
|
46 |
+
{"Query Number": 23, "Complexity Level": "More Complex", "Query Description": "List mutual funds with their volatility metrics for a specific year",
|
47 |
+
"SQL Statement": "SELECT m.fund_name, v.year, v.alpha, v.beta, v.sharpe_ratio, v.standard_deviation FROM core_mutualfund m JOIN core_mfvolatility v ON m.id = v.mutual_fund_id WHERE v.year = '2022';"},
|
48 |
+
{"Query Number": 24, "Complexity Level": "More Complex", "Query Description": "Find the mutual fund with the highest average market value per holding",
|
49 |
+
"SQL Statement": "SELECT m.fund_name, AVG(h.market_value) AS avg_market_value_per_holding FROM core_mutualfund m JOIN core_mfholdings h ON m.id = h.mutual_fund_id GROUP BY m.fund_name ORDER BY avg_market_value_per_holding DESC LIMIT 1;"},
|
50 |
+
{
|
51 |
+
"Query Number": 25,
|
52 |
+
"Complexity Level": "More Complex",
|
53 |
+
"Query Description": "Retrieve mutual funds with their total assets and the corresponding volatility metrics",
|
54 |
+
"SQL Statement": "SELECT m.fund_name, h.total_assets, v.alpha, v.beta, v.sharpe_ratio, v.standard_deviation FROM core_mutualfund m JOIN core_mfholdings h ON m.id = h.mutual_fund_id JOIN core_mfvolatility v ON m.id = v.mutual_fund_id;"
|
55 |
+
},
|
56 |
+
{
|
57 |
+
"Query Number": 26,
|
58 |
+
"Complexity Level": "More Complex",
|
59 |
+
"Query Description":"Retrieve mutual funds that have holdings in both technology and healthcare sectors, and provide a breakdown of their allocation percentages in each sector.",
|
60 |
+
"SQL Statement":"-"
|
61 |
+
},
|
62 |
+
{
|
63 |
+
"Query Number": 27,
|
64 |
+
"Complexity Level": "More Complex",
|
65 |
+
"Query Description": "Retrieve Mutual Funds with Highest Average Return and Lowest Expense Ratio",
|
66 |
+
"SQL Statement":"-"
|
67 |
+
},
|
68 |
+
{
|
69 |
+
"Query Number": 28,
|
70 |
+
"Complexity Level": "More Complex",
|
71 |
+
"Query Description":"Find Mutual Funds with Diversified Holdings",
|
72 |
+
"SQL Statement":"-"
|
73 |
+
},
|
74 |
+
{
|
75 |
+
"Query Number": 29,
|
76 |
+
"Complexity Level": "More Complex",
|
77 |
+
"Query Description": "Identify Mutual Funds with Consistent Performance and High AUM",
|
78 |
+
"SQL Statement":"-"
|
79 |
+
},
|
80 |
+
{
|
81 |
+
"Query Number": 30,
|
82 |
+
"Complexity Level": "More Complex",
|
83 |
+
"Query Description": "Calculate Weighted Average Return for Mutual Funds in a Specific Sector",
|
84 |
+
"SQL Statement":"-"
|
85 |
+
}
|
86 |
+
]
|
core/text2sql/handler.py
ADDED
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import logging
|
2 |
+
from core.models import MutualFund
|
3 |
+
from core.text2sql.ml_models import Text2SQLModel
|
4 |
+
|
5 |
+
logger = logging.getLogger(__name__)
|
6 |
+
|
7 |
+
|
8 |
+
class QueryDataHandler:
|
9 |
+
"""
|
10 |
+
A class for handling queries and fetching data using a Text2SQL model and Django models.
|
11 |
+
"""
|
12 |
+
|
13 |
+
def __init__(self):
|
14 |
+
self.mutual_fund = MutualFund()
|
15 |
+
self.text2sql_model = Text2SQLModel()
|
16 |
+
|
17 |
+
def get_data_from_query(self, prompt):
|
18 |
+
"""
|
19 |
+
Generates a PostgreSQL query using the Text2SQL model based on the input prompt
|
20 |
+
and retrieves data using Django models.
|
21 |
+
"""
|
22 |
+
# Use Text2SQL ML model to generate a PostgreSQL query
|
23 |
+
sql_query = self.text2sql_model.generate_query(prompt)
|
24 |
+
logger.info(f"SQL Query: {sql_query}")
|
25 |
+
# Use Django models to fetch data
|
26 |
+
return sql_query, self.mutual_fund.execute_query(sql_query)
|
core/text2sql/ml_models.py
ADDED
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import replicate
|
2 |
+
|
3 |
+
|
4 |
+
class Text2SQLModel:
|
5 |
+
"""
|
6 |
+
A class representing a Text-to-SQL model for generating SQL queries from LLM.
|
7 |
+
"""
|
8 |
+
|
9 |
+
def __init__(self):
|
10 |
+
pass
|
11 |
+
|
12 |
+
def load_model(self):
|
13 |
+
"""Loads the machine learning model for Text-to-SQL processing."""
|
14 |
+
pass
|
15 |
+
|
16 |
+
def generate_query(self, prompt):
|
17 |
+
output = replicate.run(
|
18 |
+
"ns-dev-sentience/sqlcoder:18bcabd866a64547daf3c6044cdebbd47a1f489571110087b80722848eb09398",
|
19 |
+
input={"prompt": prompt},
|
20 |
+
)
|
21 |
+
return (
|
22 |
+
output.split("```PostgresSQL")[-1].split("```")[0].split(";")[0].strip()
|
23 |
+
+ ";"
|
24 |
+
)
|
core/text2sql/prompt.py
ADDED
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
def get_prompt(question):
|
2 |
+
database_schema = """
|
3 |
+
CREATE TABLE core_mutualfund (
|
4 |
+
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
5 |
+
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
6 |
+
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
7 |
+
fund_name VARCHAR(200) UNIQUE,
|
8 |
+
isin_number VARCHAR(50) UNIQUE,
|
9 |
+
security_id VARCHAR(50) UNIQUE,
|
10 |
+
data JSONB,
|
11 |
+
rank INTEGER UNIQUE,
|
12 |
+
crisil_rank INTEGER,
|
13 |
+
aum DOUBLE PRECISION,
|
14 |
+
expense_ratio DOUBLE PRECISION,
|
15 |
+
nav DOUBLE PRECISION,
|
16 |
+
return_m12 DOUBLE PRECISION
|
17 |
+
);
|
18 |
+
|
19 |
+
CREATE TABLE core_mfholdings (
|
20 |
+
"id" uuid NOT NULL PRIMARY KEY,
|
21 |
+
"isin_number" varchar(20) NULL,
|
22 |
+
"security_id" varchar(20) NULL,
|
23 |
+
"sector" varchar(50) NULL,
|
24 |
+
"country" varchar(50) NULL,
|
25 |
+
"currency" varchar(100) NULL,
|
26 |
+
"weighting" double precision NULL,
|
27 |
+
"sector_code" varchar(100) NULL,
|
28 |
+
"holding_type" varchar(100) NULL,
|
29 |
+
"market_value" double precision NULL,
|
30 |
+
"stock_rating" varchar(100) NULL,
|
31 |
+
"total_assets" double precision NULL,
|
32 |
+
"currency_name" varchar(150) NULL,
|
33 |
+
"holding_name" varchar(100) NULL,
|
34 |
+
"holding_type" varchar(100) NULL,
|
35 |
+
"holding_type_id" varchar(100) NULL,
|
36 |
+
"number_of_shares" double precision NULL,
|
37 |
+
"one_year_return" double precision NULL,
|
38 |
+
"mutual_fund_id" uuid NOT NULL REFERENCES "core_mutualfund" ("id") DEFERRABLE INITIALLY DEFERRED
|
39 |
+
);
|
40 |
+
|
41 |
+
CREATE TABLE core_mfvolatility (
|
42 |
+
"id" uuid NOT NULL PRIMARY KEY,
|
43 |
+
"mutual_fund_id" uuid NOT NULL REFERENCES "core_mutualfund" ("id") DEFERRABLE INITIALLY DEFERRED,
|
44 |
+
"year" varchar(100) NOT NULL,
|
45 |
+
"alpha" double precision NULL,
|
46 |
+
"beta" double precision NULL,
|
47 |
+
"sharpe_ratio" double precision NULL,
|
48 |
+
"standard_deviation" double precision NULL
|
49 |
+
);
|
50 |
+
|
51 |
+
-- core_mfvolatility.mutual_fund_id can be joined with core_mutualfund.id
|
52 |
+
-- core_mfholdings.mutual_fund_id can be joined with core_mutualfund.id
|
53 |
+
"""
|
54 |
+
|
55 |
+
sql_prompt = f"""
|
56 |
+
Your task is to convert a question into a PostgresSQL query, given a database schema.
|
57 |
+
|
58 |
+
###Task:
|
59 |
+
Generate a SQL query that answers the question `{question}`.
|
60 |
+
|
61 |
+
### Database Schema:
|
62 |
+
This query will run on a database whose schema is represented below:
|
63 |
+
{database_schema}
|
64 |
+
|
65 |
+
### Response:
|
66 |
+
Based on your instructions, here is the PostgresSQL query I have generated to answer the question `{question}`:
|
67 |
+
```PostgresSQL
|
68 |
+
"""
|
69 |
+
return sql_prompt
|
core/urls.py
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from django.urls import path
|
2 |
+
from core.views import get_scores, get_mf_data
|
3 |
+
|
4 |
+
urlpatterns = [
|
5 |
+
path("mutual-fund-details/", get_scores, name="mutual-fund-details"),
|
6 |
+
path("get-mf-data/", get_mf_data, name="get-mf-data"),
|
7 |
+
]
|
core/views.py
ADDED
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
|
3 |
+
"""
|
4 |
+
import logging
|
5 |
+
from django.http import JsonResponse
|
6 |
+
from core.mfrating.score_calculator import MutualFundScorer
|
7 |
+
from core.text2sql.handler import QueryDataHandler
|
8 |
+
from core.text2sql.prompt import get_prompt
|
9 |
+
|
10 |
+
logger = logging.getLogger(__name__)
|
11 |
+
|
12 |
+
|
13 |
+
def get_scores(request):
|
14 |
+
"""
|
15 |
+
Retrieves scores for mutual funds based on various factors.
|
16 |
+
"""
|
17 |
+
data = MutualFundScorer().get_scores()
|
18 |
+
return JsonResponse({"status": "success", "data": data}, status=200)
|
19 |
+
|
20 |
+
|
21 |
+
def get_mf_data(request):
|
22 |
+
"""
|
23 |
+
Retrieves mutual fund data based on user query.
|
24 |
+
"""
|
25 |
+
query = request.GET.get("query", "")
|
26 |
+
logger.info(f"Query: {query}")
|
27 |
+
prompt = get_prompt(query)
|
28 |
+
logger.info(f"Prompt: {prompt}")
|
29 |
+
query, data = QueryDataHandler().get_data_from_query(prompt)
|
30 |
+
return JsonResponse({"status": "success", "query": query, "data": data}, status=200)
|
data_pipeline/__init__.py
ADDED
File without changes
|
data_pipeline/admin.py
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
from django.contrib import admin
|
2 |
+
|
3 |
+
# Register your models here.
|
data_pipeline/apps.py
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from django.apps import AppConfig
|
2 |
+
|
3 |
+
|
4 |
+
class DataPipelineConfig(AppConfig):
|
5 |
+
default_auto_field = "django.db.models.BigAutoField"
|
6 |
+
name = "data_pipeline"
|
data_pipeline/interfaces/__init__.py
ADDED
File without changes
|
data_pipeline/interfaces/api_client.py
ADDED
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Interface for API clients
|
3 |
+
"""
|
4 |
+
from abc import ABC, abstractmethod
|
5 |
+
|
6 |
+
|
7 |
+
class DataClient(ABC):
|
8 |
+
"""
|
9 |
+
Abstract class for API clients
|
10 |
+
"""
|
11 |
+
|
12 |
+
@abstractmethod
|
13 |
+
def extract(self):
|
14 |
+
"""
|
15 |
+
Extract data from API
|
16 |
+
"""
|
17 |
+
if self.api_url is None:
|
18 |
+
raise Exception("No API URL provided")
|
19 |
+
|
20 |
+
def transform(self):
|
21 |
+
"""
|
22 |
+
Placeholder method for future transformation logic.
|
23 |
+
"""
|
24 |
+
self.transformed_data = self.api_response
|
25 |
+
|
26 |
+
@abstractmethod
|
27 |
+
def load(self):
|
28 |
+
"""
|
29 |
+
Load data into target model
|
30 |
+
"""
|
31 |
+
raise Exception("No model to transform")
|
32 |
+
|
33 |
+
def run(self) -> None:
|
34 |
+
self.extract()
|
35 |
+
self.transform()
|
36 |
+
self.load()
|
data_pipeline/interfaces/test_api_client.py
ADDED
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
|
3 |
+
"""
|
4 |
+
from django.test import TestCase
|
5 |
+
from data_pipeline.interfaces.api_client import DataClient
|
6 |
+
|
7 |
+
|
8 |
+
class TestDataClient(TestCase):
|
9 |
+
def test_extract_raises_exception_on_instantiation(self):
|
10 |
+
with self.assertRaises(TypeError):
|
11 |
+
data_client = DataClient()
|
12 |
+
|
13 |
+
def test_extract_raises_exception_without_api_url(self):
|
14 |
+
class NewDataClientWithoutFunctionOverride(DataClient):
|
15 |
+
def __init__(self) -> None:
|
16 |
+
pass
|
17 |
+
|
18 |
+
with self.assertRaises(TypeError):
|
19 |
+
data_client = NewDataClientWithoutFunctionOverride()
|
20 |
+
|
21 |
+
def test_inherited_class(self):
|
22 |
+
class NewDataClientWith3FunctionOverride(DataClient):
|
23 |
+
def __init__(self) -> None:
|
24 |
+
pass
|
25 |
+
|
26 |
+
def extract(self):
|
27 |
+
pass
|
28 |
+
|
29 |
+
def transform(self):
|
30 |
+
pass
|
31 |
+
|
32 |
+
def load(self):
|
33 |
+
pass
|
34 |
+
|
35 |
+
data_client = NewDataClientWith3FunctionOverride()
|