Skip to content

Commit 4b0709c

Browse files
committed
changes feature
1 parent 29e6103 commit 4b0709c

File tree

29 files changed

+381
-138
lines changed

29 files changed

+381
-138
lines changed

backend/routers/filer.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,12 +182,14 @@ def create_historical(cik, company, stamp):
182182
database.edit_filer(filer_query, stock_query)
183183
database.add_log(cik, log_item)
184184

185+
change_list = analysis.analyze_changes(cik)
185186
allocation_list = analysis.analyze_allocation(cik)
186187
aum_list = analysis.analyze_aum(cik)
187188
database.edit_filer(
188189
{"cik": cik},
189190
{
190191
"$set": {
192+
"analysis.changes": change_list,
191193
"analysis.allocation": allocation_list,
192194
"analysis.aum_timeseries": aum_list,
193195
}

backend/routers/filing.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ async def record_filing_csv(cik: str, access_number: str, headers: str = None):
165165
)
166166

167167

168+
@cache(2)
168169
@router.get("/filer", status_code=200)
169170
async def filings_info(cik: str):
170171
pipeline = [
@@ -179,6 +180,7 @@ async def filings_info(cik: str):
179180
return {"filings": filings}
180181

181182

183+
@cache(2)
182184
@router.get("/info", status_code=200)
183185
async def filing_info(cik: str, access_number: str, include_filer: bool = False):
184186
filing = database.find_filing(cik, access_number, {"_id": 0, "cik": 0, "stocks": 0})
@@ -199,3 +201,12 @@ async def filing_info(cik: str, access_number: str, include_filer: bool = False)
199201
"filing": filing,
200202
**({"filer": filer} if include_filer else {}),
201203
}
204+
205+
206+
@router.get("/changes", status_code=200)
207+
async def changes(cik: str, access_number: str):
208+
filing = database.find_filing(cik, access_number)
209+
if filing is None:
210+
raise HTTPException(404, detail="Filing not found.")
211+
212+
filing_forms = web.check_forms(cik)

backend/routers/general.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88
from worker import tasks as worker
99

10-
1110
from .lib import database
1211
from .lib import cache as cm
1312
from .lib.backup import save_collections
@@ -202,6 +201,18 @@ async def repair_all_filers(
202201
return {"description": "Started repairing all filers."}
203202

204203

204+
# from .lib import analysis
205+
# @router.get("/test", status_code=200, include_in_schema=False)
206+
# async def test_route(password: str, background: BackgroundTasks = BackgroundTasks):
207+
# if password != ADMIN_PASSWORD:
208+
# raise HTTPException(detail="Unable to give access.", status_code=403)
209+
210+
# changes = analysis.analyze_changes("1037389")
211+
# print(changes)
212+
213+
# return {"description": "Hello World!"}
214+
215+
205216
@cache
206217
@router.get("/favicon.ico", status_code=200)
207218
async def favicon():

backend/routers/lib/analysis.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
import logging
66

77
from datetime import datetime
8+
from collections import defaultdict
89

910
from . import database
1011
from . import api
12+
from . import web
1113
from . import cache
1214
from . import errors
1315

@@ -872,6 +874,122 @@ def sort_and_format(filer_ciks):
872874
raise KeyError
873875

874876

877+
def analyze_changes(cik):
878+
879+
pipeline = [
880+
{"$match": {"cik": cik, "form": {"$in": database.holding_forms}}},
881+
{
882+
"$project": {
883+
"access_number": 1,
884+
"stocks": {"$objectToArray": "$stocks"},
885+
}
886+
},
887+
{"$project": {"access_number": 1, "stock": "$stocks.v"}},
888+
{"$unwind": "$stock"},
889+
{
890+
"$project": {
891+
"access_number": 1,
892+
"stock.cusip": 1,
893+
"stock.shares_held": 1,
894+
"stock.market_value": 1,
895+
}
896+
},
897+
{
898+
"$group": {
899+
"_id": "$access_number",
900+
"access_number": {"$first": "$access_number"},
901+
"stocks": {"$push": "$stock"},
902+
}
903+
},
904+
{
905+
"$set": {
906+
"access_number": "$_id",
907+
}
908+
},
909+
{
910+
"$project": {
911+
"_id": 0,
912+
"access_number": 1,
913+
"stocks": {
914+
"$arrayToObject": {
915+
"$map": {
916+
"input": "$stocks",
917+
"as": "stock",
918+
"in": {"k": "$$stock.cusip", "v": "$$stock"},
919+
}
920+
}
921+
},
922+
}
923+
},
924+
]
925+
filings_reports = [f["access"] for f in web.check_forms(cik)]
926+
filings_list = [f for f in database.search_filings(pipeline)]
927+
928+
changes_list = []
929+
for next_filing in filings_list:
930+
931+
next_access = next_filing["access_number"]
932+
next_index = filings_reports.index(next_access)
933+
934+
prev_index = next_index + 1 if next_index + 1 < len(filings_reports) else None
935+
if prev_index is None:
936+
changes_list.append(
937+
{
938+
"access_number": next_access,
939+
"changes": [],
940+
}
941+
)
942+
continue
943+
prev_access = filings_reports[prev_index]
944+
prev_filing = next(
945+
(f for f in filings_list if f["access_number"] == prev_access),
946+
None,
947+
)
948+
949+
if not prev_filing:
950+
continue
951+
952+
next_stocks = next_filing.get("stocks", {})
953+
prev_stocks = prev_filing.get("stocks", {})
954+
955+
if not next_stocks or not prev_stocks:
956+
continue
957+
958+
change = defaultdict(lambda: {"shares": {}, "value": {}})
959+
for cusip in next_stocks:
960+
next_stock = next_stocks[cusip]
961+
prev_stock = prev_stocks.get(cusip, None)
962+
963+
next_shares = next_stock["shares_held"]
964+
prev_shares = prev_stock["shares_held"] if prev_stock else 0
965+
if next_shares != prev_shares:
966+
change[cusip]["shares"]["amount"] = abs(next_shares - prev_shares)
967+
change[cusip]["shares"]["type"] = (
968+
"buy" if next_shares > prev_shares else "sell"
969+
)
970+
971+
next_value = next_stock["market_value"]
972+
prev_value = prev_stock["market_value"] if prev_stock else 0
973+
if next_value != prev_value:
974+
change[cusip]["value"]["amount"] = abs(next_value - prev_value)
975+
change[cusip]["value"]["type"] = (
976+
"buy" if next_value > prev_value else "sell"
977+
)
978+
change = dict(change)
979+
980+
database.edit_filing(
981+
{"cik": cik, "access_number": next_access}, {"$set": {"changes": change}}
982+
)
983+
changes_list.append(
984+
{
985+
"access_number": next_access,
986+
"changes": change,
987+
}
988+
)
989+
990+
return changes_list
991+
992+
875993
# Really janky/inefficient
876994

877995

backend/routers/lib/web.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ def process_names(stocks, cik):
8787
return global_stocks
8888

8989

90-
def check_new(cik):
90+
def check_new(cik: str):
9191
document_reports = check_forms(cik)
9292

9393
latest_report = document_reports[-1]
@@ -105,7 +105,7 @@ def check_new(cik):
105105
return False, None
106106

107107

108-
def check_forms(cik):
108+
def check_forms(cik: str):
109109
data = api.sec_filer_search(cik)
110110
recent_filings = data["filings"]["recent"]
111111

frontend/components/Analysis/Analysis.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useState } from "react";
44
import FolderIcon from "@/public/static/folder.svg";
55
import FilterIcon from "@/public/static/filter.svg";
66

7-
import { fontLight } from "@fonts";
7+
import { font as fontLight } from "@fonts"; // lazyness
88

99
const Analysis = ({ text = "Analysis", icon = "folder", children }) => {
1010
const [open, setOpen] = useState(false);

frontend/components/Analysis/Analysis.module.css

Lines changed: 2 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: 19%;
21+
width: 20%;
2222
border-radius: 5px;
23-
padding: 5px;
23+
padding: 5px 7px;
2424
opacity: 1;
2525
margin: auto;
2626
margin-top: 10px;

frontend/components/Explorer/Timeline/Select/Picker/Picker.jsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,18 @@ const Picker = (props) => {
2626
const accessNumber = filing.access_number;
2727
const reportDate = new Date(
2828
filing.report_date * 1000
29-
).toLocaleDateString();
29+
).toLocaleDateString("en-US", {
30+
month: "short",
31+
day: "numeric",
32+
year: "numeric",
33+
});
3034
const filingDate = new Date(
3135
filing.filing_date * 1000
32-
).toLocaleDateString();
36+
).toLocaleDateString("en-US", {
37+
month: "short",
38+
day: "numeric",
39+
year: "numeric",
40+
});
3341
const marketValue = filing.market_value
3442
? new Intl.NumberFormat("en-US").format(filing.market_value)
3543
: "-";

frontend/components/Explorer/Timeline/Select/Picker/Picker.module.css

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,15 +62,17 @@
6262
justify-content: space-between;
6363
cursor: pointer;
6464

65-
width: 95%;
65+
width: 100%;
6666
border-radius: 5px;
67+
border-bottom: solid var(--offwhite) 1px;
6768

6869
color: var(--secondary-dark);
69-
transition: background-color 0.1s ease;
70+
transition: background-color 0.1s ease, border-radius 0.3s ease;
7071
}
7172

7273
.picker-filing:hover {
7374
background-color: var(--offwhite);
75+
border-radius: 0px;
7476
}
7577

7678
.filing-attribute {
@@ -79,11 +81,19 @@
7981
justify-content: center;
8082
text-align: center;
8183

84+
font-size: 0.8rem;
8285
width: 30%;
8386
margin: 5px;
8487
border-radius: 5px;
88+
border-right: solid var(--offwhite) 1px;
89+
}
8590

86-
background-color: var(--offwhite);
91+
.filing-attribute:last-child {
92+
border-right: none;
93+
}
94+
95+
.filing-attribute:first-child {
96+
font-size: 1.2rem;
8797
}
8898

8999
.picker-attributes {

frontend/components/Explorer/Timeline/Select/Record/Record.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ const Record = (props) => {
4343
}
4444
>
4545
<span className={font.className}>
46-
{variant == "csv" ? "Table" : "Data"}
46+
{variant == "csv" ? "Spreadsheet" : "JSON"}
4747
</span>
4848
{variant == "csv" ? (
4949
<TableIcon className={styles["record-icon"]} />

0 commit comments

Comments
 (0)