Skip to content

Commit bc4a4ed

Browse files
committed
top holdings and other feats
1 parent 9f6dc86 commit bc4a4ed

File tree

41 files changed

+785
-253
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+785
-253
lines changed

backend/routers/filer.py

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ class HTTPError(BaseModel):
6262
# 0 : Filer is done being built, logs are no longer kept.
6363

6464
# Different Filer Stages
65+
# 5 : Filer is broken, and has no query data. Will never be updated unless manually.
6566
# 4 : Filer was non-existent before and is being fully built from the ground up.
6667
# 3 : Filer still building, but estimation time has been calculated.
6768
# 2 : Filer's newest filing is built, but older filings are still being updated and sorted. Estimation time is calculated.
@@ -79,12 +80,21 @@ def create_recent(cik, company, stamp):
7980
last_report = company["last_report"]
8081
recent_filing = database.find_filing(cik, last_report)
8182

82-
for access_number, filing_stocks in web.process_stocks(cik, [recent_filing]):
83-
recent_filing["stocks"] = filing_stocks
84-
database.edit_filing(
85-
{**filer_query, "access_number": access_number},
86-
{"$set": {"stocks": filing_stocks}},
87-
)
83+
try:
84+
for access_number, filing_stocks in web.process_stocks(
85+
cik, [recent_filing]
86+
):
87+
recent_filing["stocks"] = filing_stocks
88+
database.edit_filing(
89+
{**filer_query, "access_number": access_number},
90+
{"$set": {"stocks": filing_stocks}},
91+
)
92+
except (
93+
IndexError
94+
) as e: # Filer is broken, no forms found. Special case for first query.
95+
break_filer(cik)
96+
report_error(cik, e)
97+
raise HTTPException(404, detail="Forms not found for filer.")
8898

8999
database.add_log(cik, "Queried Filer Recent Stocks", company_name, cik)
90100
except Exception as e:
@@ -353,6 +363,15 @@ async def rollback_filer(
353363
return {"description": "Filer rollback started."}
354364

355365

366+
async def break_filer(cik: str):
367+
document_reports = web.check_forms(cik)
368+
if len(document_reports) == 0:
369+
database.edit_status(cik, 5)
370+
database.add_log(cik, "Filer has no filings.", "Error", cik)
371+
else:
372+
report_error(cik, Exception("Filer is broken but has forms."))
373+
374+
356375
@cache(24)
357376
@router.get("/search", tags=["filers"], status_code=200)
358377
async def search_filers(q: str, limit: int = 4):
@@ -475,6 +494,8 @@ async def filer_info(cik: str):
475494
raise HTTPException(404, detail="Filer not found.")
476495

477496
status = database.find_log(cik, {"status": 1, "_id": 0})
497+
if status is None:
498+
raise HTTPException(404, detail="Filer log not found.")
478499
filer["status"] = status["status"]
479500

480501
return {"description": "Found filer.", "filer": filer}

backend/routers/filing.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,12 +180,22 @@ async def filings_info(cik: str):
180180

181181

182182
@router.get("/info", status_code=200)
183-
async def filing_info(cik: str, access_number: str):
183+
async def filing_info(cik: str, access_number: str, include_filer: bool = False):
184184
filing = database.find_filing(cik, access_number, {"_id": 0, "cik": 0, "stocks": 0})
185185
if filing is None:
186186
raise HTTPException(detail="Filing not found.", status_code=404)
187187

188+
filer = database.find_filer(cik, {"_id": 0, "stocks": 0}) if include_filer else None
189+
if filer is None and include_filer:
190+
raise HTTPException(404, detail="Filer not found.")
191+
188192
status = database.find_log(cik, {"status": 1, "_id": 0})
193+
if status is None:
194+
raise HTTPException(404, detail="Filer log not found.")
189195
filing["status"] = status["status"]
190196

191-
return {"description": "Filing found.", "filing": filing}
197+
return {
198+
"description": "Filing found.",
199+
"filing": filing,
200+
**({"filer": filer} if include_filer else {}),
201+
}

backend/routers/lib/analysis.py

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -143,22 +143,22 @@ def serialize_global(local_stock, global_stock):
143143
)
144144
portfolio_percentage_str = (
145145
"{:.2f}".format(round(portfolio_percentage, 4))
146-
if portfolio_percentage != "N/A"
146+
if portfolio_percentage != "N/A" and type(portfolio_percentage) == float
147147
else "N/A"
148148
)
149149
ownership_percentage_str = (
150150
"{:.2f}".format(round(ownership_percentage, 4))
151-
if ownership_percentage != "N/A"
151+
if ownership_percentage != "N/A" and type(ownership_percentage) == float
152152
else "N/A"
153153
)
154154
gain_value_str = (
155155
"{:.2f}".format(round(gain_value, 2))
156-
if update and buy_timeseries != "N/A"
156+
if update and buy_timeseries != "N/A" and type(gain_value) == float
157157
else "N/A"
158158
)
159159
gain_percent_str = (
160160
"{:.2f}".format(round(gain_percent, 2))
161-
if update and buy_timeseries != "N/A"
161+
if update and buy_timeseries != "N/A" and type(gain_percent) == float
162162
else "N/A"
163163
)
164164

@@ -303,13 +303,21 @@ def serialize_local(
303303

304304

305305
def analyze_total(cik, stocks, access_number):
306-
market_values = []
306+
market_map = []
307307
for key in stocks:
308308
stock = stocks[key]
309309
value = stock.get("market_value", 0)
310-
market_values.append(value)
311-
310+
market_map.append({"cusip": key, "market_value": value})
311+
312+
market_values = [stock["market_value"] for stock in market_map]
313+
top_holdings = [
314+
stock["cusip"]
315+
for stock in sorted(market_map, key=lambda x: x["market_value"], reverse=True)[
316+
:5
317+
][: min(5, len(market_map))]
318+
]
312319
total = sum(market_values)
320+
313321
database.edit_filing(
314322
{
315323
"cik": cik,
@@ -319,6 +327,7 @@ def analyze_total(cik, stocks, access_number):
319327
{
320328
"$set": {
321329
"market_value": total,
330+
"top_holdings": top_holdings,
322331
}
323332
},
324333
)
@@ -621,14 +630,27 @@ def sort_pipeline(
621630
reverse: bool,
622631
unavailable: bool,
623632
additional: list = [],
633+
stock_structure: str = "array",
624634
collection_search=database.search_filers,
635+
match_query={},
625636
):
626637
if limit < 0:
627638
raise ValueError
628639

629640
pipeline = [
630-
{"$match": {"cik": cik}},
641+
{
642+
"$match": {"cik": cik, **match_query},
643+
},
631644
]
645+
646+
if stock_structure == "dict":
647+
pipeline.extend(
648+
[
649+
{"$set": {"stocks": {"$objectToArray": "$stocks"}}},
650+
{"$set": {"stocks": "$stocks.v"}},
651+
]
652+
)
653+
632654
if additional:
633655
pipeline.extend(additional)
634656

@@ -646,15 +668,18 @@ def sort_pipeline(
646668
if unavailable is False:
647669
sort_query = f"${sort}"
648670

671+
pipeline.append({"$project": {"_id": 1}})
649672
cursor = collection_search(pipeline)
650673
results = [result for result in cursor]
651674
if not cursor or not results:
652675
raise LookupError
653676
count = len(results)
654677

678+
pipeline.pop(-1)
655679
pipeline.append(
656680
{"$sort": {sort: 1 if reverse else -1, "_id": 1}},
657681
)
682+
658683
if unavailable is False:
659684
sort_stage = pipeline.pop(-1)
660685
pipeline.extend(

backend/routers/lib/database.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,18 @@ def search_filers(pipeline):
134134
return cursor
135135

136136

137+
@retry_on_rate_limit()
138+
def search_filings(pipeline):
139+
cursor = filings.aggregate(pipeline)
140+
return cursor
141+
142+
143+
@retry_on_rate_limit()
144+
def search_filings(pipeline):
145+
cursor = filings.aggregate(pipeline)
146+
return cursor
147+
148+
137149
@retry_on_rate_limit()
138150
def search_filer(cik, project={"_id": 0}):
139151
cursor = main.aggregate(pipeline=[{"$match": {"cik": cik}}, {"$project": project}])
@@ -197,12 +209,6 @@ def map_filings(cik, key="access_number", project={"_id": 0}, form_type="13F-HR"
197209
return results_dict
198210

199211

200-
@retry_on_rate_limit()
201-
def search_filings(pipeline):
202-
cursor = filings.aggregate(pipeline)
203-
return cursor
204-
205-
206212
@retry_on_rate_limit()
207213
def add_filings(filing_list):
208214
filings.insert_many(filing_list)

backend/routers/stocks.py

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ async def query_stocks(cik: str, background: BackgroundTasks):
4040

4141

4242
@cache
43-
@router.get("/info", tags=["stocks", "filers"], status_code=200)
44-
async def stock_info(
43+
@router.get("/filer", tags=["stocks", "filers"], status_code=200)
44+
async def stock_filer(
4545
cik: str,
4646
limit: int,
4747
offset: int,
@@ -77,6 +77,57 @@ async def stock_info(
7777
)
7878

7979

80+
@cache
81+
@router.get("/filing", tags=["stocks", "filings"], status_code=200)
82+
async def stock_filing(
83+
cik: str,
84+
access_number: str,
85+
limit: int,
86+
offset: int,
87+
sort: str,
88+
sold: bool,
89+
reverse: bool,
90+
unavailable: bool,
91+
):
92+
filer = database.find_filer(cik, {"_id": 1})
93+
if not filer:
94+
raise HTTPException(detail="Filer not found.", status_code=404)
95+
96+
try:
97+
pipeline, count = analysis.sort_pipeline(
98+
cik,
99+
limit,
100+
offset,
101+
sort,
102+
sold,
103+
reverse,
104+
unavailable,
105+
stock_structure="dict",
106+
collection_search=database.search_filings,
107+
match_query={
108+
"access_number": access_number,
109+
"stocks": {"$exists": True},
110+
},
111+
)
112+
cursor = database.search_filings(pipeline)
113+
except LookupError as e:
114+
errors.report_error(cik, e)
115+
raise HTTPException(detail="No results found.", status_code=422)
116+
except Exception as e:
117+
errors.report_error(cik, e)
118+
cursor = []
119+
count = 0
120+
121+
try:
122+
stock_list = [result for result in cursor]
123+
except KeyError:
124+
raise HTTPException(detail="Error while searching.", status_code=500)
125+
126+
return BrowserCachedResponse(
127+
content={"stocks": stock_list, "count": count}, cache_hours=cache_time
128+
)
129+
130+
80131
@cache(4)
81132
@router.get("/timeseries", tags=["stocks", "filers"], status_code=200)
82133
async def stock_timeseries(cik: str, time: float):

backend/static/statistics.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
{
22
"latest": {
3-
"count": 1699,
4-
"total": 1271220.7323975563,
5-
"average": 748.2170290744887
3+
"count": 1718,
4+
"total": 1273623.8951702118,
5+
"average": 741.3410332771896
66
},
77
"historical": {
8-
"count": 1684,
9-
"total": 5897062.192974329,
10-
"average": 3501.818404379055
8+
"count": 1695,
9+
"total": 5920188.0643363,
10+
"average": 3492.73632114236
1111
}
1212
}

frontend/components/Analysis/Analysis.jsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ const Analysis = ({ text = "Analysis", icon = "folder", children }) => {
3333
styles["analysis-dummy"],
3434
open ? styles["dummy-expand"] : styles["dummy-contract"],
3535
].join(" ")}
36-
></div>
36+
>
37+
<span className={styles["dummy-text"]}>(Tools and Downloads)</span>
38+
</div>
3739
<span
3840
className={[styles["analysis-tip"], fontLight.className].join(" ")}
3941
>

frontend/components/Analysis/Analysis.module.css

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@
1818
align-items: center;
1919
cursor: pointer;
2020

21-
width: 10%;
21+
width: 19%;
2222
border-radius: 5px;
23-
padding: 10px;
23+
padding: 5px;
2424
opacity: 1;
2525
margin: auto;
2626
margin-top: 10px;
@@ -43,6 +43,17 @@
4343
height: 20px;
4444
width: 1%;
4545
transition: width 0.5s ease;
46+
position: relative;
47+
}
48+
49+
.dummy-text {
50+
display: none;
51+
position: absolute;
52+
transition: opacity 0.3s ease;
53+
}
54+
55+
.analysis-expanded .dummy-text {
56+
opacity: 0;
4657
}
4758

4859
.dummy-expand {

0 commit comments

Comments
 (0)