Skip to content

Commit 2008106

Browse files
authored
Merge pull request #20 from leftmove/development
2 parents f2b44c2 + 4861b69 commit 2008106

Some content is hidden

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

48 files changed

+4350
-1458
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
.DS_Store
2-
.vscode
2+
.vscode
3+
.gitignore

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ The problem though, is that these holdings are often cumbersome to access, and v
4242

4343
This repository holds the [backend](./backend/) and [frontend](./frontend/) for wallstreetlocal.
4444

45-
To visit the wallstreetlocal, you can go to [`wallstreetlocal.com`](https://wallstreetlocal.com).
45+
To visit wallstreetlocal, you can go to [`wallstreetlocal.com`](https://wallstreetlocal.com).
4646

4747
You can also see important resources used to create the site at the [resources](https://www.wallstreetlocal.com/about/resources) page, or view the OpenAPI documentation at the [API](https://content.wallstreetlocal.com/docs) page.
4848

backend/routers/filer.py

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
import json
66
import os
77
import logging
8+
import re
89
from urllib import parse
910
from datetime import datetime
1011

1112
from worker import tasks as worker
13+
from .responses import BrowserCachedResponse
1214

1315
from .lib import web
1416
from .lib import database
@@ -501,6 +503,259 @@ async def filer_info(cik: str):
501503
return {"description": "Found filer.", "filer": filer}
502504

503505

506+
def convert_title(d):
507+
if d:
508+
d = re.sub(
509+
r"(^\w|\s\w)(\S*)",
510+
lambda m: (
511+
m.group(1).upper() + m.group(2).lower()
512+
if not re.search(r"[a-z][A-Z]|[A-Z][a-z]", m.group(1) + m.group(2))
513+
else m.group(1) + m.group(2)
514+
),
515+
d,
516+
)
517+
for word in ["LLC", "LP", "L.P.", "LLP", "N.A."]:
518+
d = d.replace(word.capitalize(), word)
519+
return d
520+
521+
522+
def snake_to_camel(s: dict):
523+
def to_camel_case(snake_str):
524+
components = snake_str.split("_")
525+
return components[0] + "".join(x.title() for x in components[1:])
526+
527+
def convert_keys(obj):
528+
if isinstance(obj, dict):
529+
new_obj = {}
530+
for k, v in obj.items():
531+
new_obj[to_camel_case(k)] = convert_keys(v)
532+
return new_obj
533+
elif isinstance(obj, list):
534+
return [convert_keys(i) for i in obj]
535+
else:
536+
return obj
537+
538+
return convert_keys(s)
539+
540+
541+
people_dict = {
542+
"1067983": ["Warren Buffet"],
543+
"1167483": ["Chase Coleman", "Scott Shleifer"],
544+
"1336528": ["Bill Ackman"],
545+
"1350694": ["Ray Dalio"],
546+
} # Change later
547+
548+
549+
@cache(24)
550+
@router.get("/preview", tags=["filers"], status_code=200)
551+
async def filer_preview(cik: str, holding_count: int = 5):
552+
553+
if holding_count > 10:
554+
raise HTTPException(422, detail="Holding count cannot exceed 10.")
555+
556+
filer = database.find_filer(
557+
cik,
558+
{
559+
"_id": 0,
560+
"cik": 1,
561+
"name": 1,
562+
"tickers": 1,
563+
"market_value": 1,
564+
"last_report": 1,
565+
},
566+
)
567+
if filer is None:
568+
raise HTTPException(404, detail="Filer not found.")
569+
570+
status = database.find_log(cik, {"status": 1, "_id": 0})
571+
if status is None:
572+
raise HTTPException(404, detail="Filer log not found.")
573+
filer["status"] = status["status"]
574+
575+
filer_cik = filer["cik"]
576+
filer["name"] = convert_title(filer["name"])
577+
578+
if filer_cik in people_dict:
579+
filer["people"] = people_dict[filer_cik]
580+
else:
581+
filer["people"] = filer.get("people", [])
582+
583+
top_holding_length = 5 + 1
584+
holding_count = holding_count + 1
585+
586+
last_report = filer["last_report"]
587+
filing = [
588+
result
589+
for result in database.search_filings(
590+
[
591+
{"$match": {"cik": filer_cik, "access_number": last_report}},
592+
{
593+
"$addFields": {
594+
"stocks": {"$objectToArray": "$stocks"},
595+
}
596+
},
597+
{"$unwind": "$stocks"},
598+
{"$sort": {"stocks.v.market_value": -1}},
599+
{"$limit": 5},
600+
{
601+
"$group": {
602+
"_id": "$_id",
603+
"stocks": {"$push": {"k": "$stocks.k", "v": "$stocks.v"}},
604+
"report_date": {"$first": "$report_date"},
605+
}
606+
},
607+
{
608+
"$addFields": {
609+
"stocks": {"$arrayToObject": "$stocks"},
610+
}
611+
},
612+
{"$project": {"_id": 0, "stocks": 1, "report_date": 1}},
613+
]
614+
)
615+
][0]
616+
raw_holdings = [
617+
s["cusip"]
618+
for s in sorted(
619+
filing["stocks"].values(),
620+
key=lambda x: x.get("market_value", 0),
621+
reverse=True,
622+
)[: min(holding_count, len(filing["stocks"]))]
623+
]
624+
filer["date"] = datetime.fromtimestamp(filing["report_date"]).strftime("%B %d, %Y")
625+
626+
stock_list = [
627+
{
628+
**s,
629+
"market_value": filing["stocks"][s["cusip"]]["market_value"],
630+
"portfolio_percent": filing["stocks"][s["cusip"]]["ratios"][
631+
"portfolio_percent"
632+
],
633+
}
634+
for s in database.find_stocks(
635+
"cusip",
636+
{"$in": raw_holdings},
637+
{
638+
"_id": 0,
639+
"cusip": 1,
640+
"name": 1,
641+
"ticker": 1,
642+
},
643+
)
644+
]
645+
filer["top_holdings"] = [
646+
s["ticker"] for s in stock_list[: min(top_holding_length, len(stock_list))]
647+
] # Doesn't handle for no tickers
648+
filer["holdings"] = stock_list
649+
filer = snake_to_camel(filer)
650+
651+
return {"description": "Filer preview loaded.", "filer": filer}
652+
653+
654+
def get_sample_filers():
655+
cik_list = [
656+
"1067983",
657+
"1167483",
658+
"102909",
659+
"1350694",
660+
"1336528",
661+
"1364742",
662+
"93751",
663+
"19617",
664+
"73124",
665+
"884546",
666+
]
667+
filer_list = database.find_filers(
668+
{"cik": {"$in": cik_list}},
669+
{
670+
"_id": 0,
671+
"cik": 1,
672+
"name": 1,
673+
"tickers": 1,
674+
"market_value": 1,
675+
"last_report": 1,
676+
},
677+
)
678+
final_list = []
679+
680+
for filer in filer_list:
681+
682+
filer_cik = filer["cik"]
683+
filer["name"] = convert_title(filer["name"])
684+
685+
if filer_cik in people_dict:
686+
filer["people"] = people_dict[filer_cik]
687+
else:
688+
filer["people"] = []
689+
690+
last_report = filer["last_report"]
691+
filing = [
692+
result
693+
for result in database.search_filings(
694+
[
695+
{"$match": {"cik": filer_cik, "access_number": last_report}},
696+
{"$addFields": {"stocks": {"$objectToArray": "$stocks"}}},
697+
{"$unwind": "$stocks"},
698+
{"$sort": {"stocks.v.market_value": -1}},
699+
{"$limit": 5},
700+
{
701+
"$group": {
702+
"_id": "$_id",
703+
"stocks": {"$push": {"k": "$stocks.k", "v": "$stocks.v"}},
704+
}
705+
},
706+
{"$addFields": {"stocks": {"$arrayToObject": "$stocks"}}},
707+
{"$project": {"_id": 0, "stocks": 1, "report_date": 1}},
708+
]
709+
)
710+
][0]
711+
raw_holdings = [
712+
s["cusip"]
713+
for s in sorted(
714+
filing["stocks"].values(),
715+
key=lambda x: x.get("market_value", 0),
716+
reverse=True,
717+
)[: min(11, len(filing["stocks"]))]
718+
]
719+
filer["date"] = datetime.fromtimestamp(filing["report_date"]).strftime(
720+
"%B %d, %Y"
721+
)
722+
723+
stock_list = [
724+
{
725+
**s,
726+
"market_value": filing["stocks"][s["cusip"]]["market_value"],
727+
"portfolio_percent": filing["stocks"][s["cusip"]]["ratios"][
728+
"portfolio_percent"
729+
],
730+
}
731+
for s in database.find_stocks(
732+
"cusip",
733+
{"$in": raw_holdings},
734+
{
735+
"_id": 0,
736+
"cusip": 1,
737+
"name": 1,
738+
"ticker": 1,
739+
},
740+
)
741+
]
742+
filer["top_holdings"] = [
743+
s["ticker"] for s in stock_list[: min(4, len(stock_list))]
744+
] # Doesn't handle for no tickers
745+
filer["holdings"] = stock_list
746+
747+
filer = snake_to_camel(filer)
748+
749+
final_list.append(filer)
750+
751+
final_list = sorted(final_list, key=lambda x: cik_list.index(x["cik"]))
752+
753+
print("Sample Filers Loaded.")
754+
print(final_list)
755+
756+
return final_list
757+
758+
504759
@cache(24)
505760
@router.get("/record", tags=["filers", "records"], status_code=200)
506761
async def record(cik: str):

backend/routers/lib/analysis.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,11 @@ def analyze_total(cik, stocks, access_number):
311311

312312
market_values = [stock["market_value"] for stock in market_map]
313313
top_holdings = [
314-
stock["cusip"]
314+
{
315+
"cusip": stock["cusip"],
316+
"name": stock["name"],
317+
"market_value": stock["market_value"],
318+
}
315319
for stock in sorted(market_map, key=lambda x: x["market_value"], reverse=True)[
316320
:5
317321
][: min(5, len(market_map))]

backend/routers/lib/database.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,8 @@ def find_stock(field, value):
9595

9696

9797
@retry_on_rate_limit()
98-
def find_stocks(field, value):
99-
results = stocks.find({field: value}, {"_id": 0})
98+
def find_stocks(field, value, project={"_id": 0}):
99+
results = stocks.find({field: value}, project)
100100
return results
101101

102102

backend/routers/lib/web.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ def process_filings(cik, data):
179179
last_report = "N/A"
180180
first_report = "N/A"
181181
for i, form in enumerate(data_filings["form"]):
182-
if form == "13F-HR":
182+
if form in database.holding_forms:
183183
if last_report == "N/A":
184184
last_report = data_filings["accessionNumber"][i]
185185
first_report = data_filings["accessionNumber"][i]
@@ -195,7 +195,6 @@ def initialize_filer(cik, sec_data):
195195
start = datetime.now().timestamp()
196196
stamp = {
197197
**company,
198-
"logs": [],
199198
"status": 4,
200199
"time": {
201200
"remaining": 0,

frontend/bun.lockb

-81.8 KB
Binary file not shown.

frontend/components/Bar/Bar.module.css

Whitespace-only changes.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { fontLight } from "fonts";
2+
import { cn } from "components/ui/utils";
3+
4+
const Primary = ({ children, className, ...rest }) => {
5+
return (
6+
<button
7+
className={cn(
8+
"text-black-two border-2 font-switzer duration-150 border-solid transition-colors font-semibold px-2 py-1 bg-offwhite-one hover:bg-offwhite-two rounded-md text-nowrap",
9+
className
10+
)}
11+
{...rest}
12+
>
13+
{children}
14+
</button>
15+
);
16+
};
17+
18+
export default Primary;
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { fontLight } from "fonts";
2+
import { cn } from "components/ui/utils";
3+
4+
const Primary = ({ children, className, ...rest }) => {
5+
return (
6+
<button
7+
className={cn(
8+
"text-offwhite-one border-2 font-switzer duration-150 border-solid transition-colors font-semibold px-2 py-1 bg-black-one hover:bg-black-two rounded-md text-nowrap",
9+
className
10+
)}
11+
{...rest}
12+
>
13+
{children}
14+
</button>
15+
);
16+
};
17+
18+
export default Primary;

0 commit comments

Comments
 (0)