|
5 | 5 | import json
|
6 | 6 | import os
|
7 | 7 | import logging
|
| 8 | +import re |
8 | 9 | from urllib import parse
|
9 | 10 | from datetime import datetime
|
10 | 11 |
|
11 | 12 | from worker import tasks as worker
|
| 13 | +from .responses import BrowserCachedResponse |
12 | 14 |
|
13 | 15 | from .lib import web
|
14 | 16 | from .lib import database
|
@@ -501,6 +503,259 @@ async def filer_info(cik: str):
|
501 | 503 | return {"description": "Found filer.", "filer": filer}
|
502 | 504 |
|
503 | 505 |
|
| 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 | + |
504 | 759 | @cache(24)
|
505 | 760 | @router.get("/record", tags=["filers", "records"], status_code=200)
|
506 | 761 | async def record(cik: str):
|
|
0 commit comments