Scraper#

Module richy.core.scraper contains classes responsible for actuall web content scraping.

Manager#

Class Manager handles all the methods for scraping actuall items:

class Manager:
    """
    Main manager class for scraping.
    All scraping methods are placed here.
    """

    mobile_user_agent = (
        "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N)"
        "AppleWebKit/537.36 (KHTML, like Gecko)"
        "Chrome/58.0.3029.33 Mobile Safari/537.36"
    )
    desktop_user_agent = (
        "Mozilla/5.0 (X11; Linux x86_64; rv:77.0) Gecko/20100101 Firefox/77.0"
    )

    def __repr__(self):
        from pprint import pformat

        return "<" + type(self).__name__ + "> " + pformat(vars(self), indent=4)

    @staticmethod
    def get_share_basic_info(share):
        """
        Fetches share basic info via rug library.

        :param Share share: Share model instance.
        :return: Basic info as a dict.
        :rtype: dict
        """
        api = TipRanks(share.symbol)

        try:
            return api.get_basic_info()
        except SymbolNotFound:
            try:
                info = yf.Ticker(share.symbol).get_info()
                return {
                    "company_name": info["shortName"],
                    "market": "",
                    "description": info["longBusinessSummary"],
                    "market_cap": info["marketCap"],
                    "has_dividends": bool(info["dividendYield"]),
                    "yoy_change": autofloatformat(
                        info["52WeekChange"] * 100, no_str=True
                    ),
                    "year_low": info["fiftyTwoWeekLow"],
                    "year_high": info["fiftyTwoWeekHigh"],
                    "pe_ratio": info["forwardPE"],
                    "eps": info["forwardEps"],
                    "similar_stocks": [],
                }
            except:
                pass

            LOGGER.exception(f"Basic info wasn't downloaded", extra={"share": share})
        except:
            LOGGER.exception("Basic info wasn't downloaded", extra={"share": share})

        return {}

    @staticmethod
    def get_etf_basic_info(etf):
        """
        Fetches etf basic info via rug library.

        :param Etf etf: Etf model instance.
        :return: Basic info as a dict.
        :rtype: dict
        """

        api = EtfDb(etf.symbol)

        try:
            return api.get_basic_info()
        except SymbolNotFound:
            LOGGER.debug(f"Holdings weren't downloaded - symbol {etf} wasn't found.")
        except:
            LOGGER.exception("Holdings weren't downloaded", extra={"etf": etf})

        return {}

    @staticmethod
    def get_coin_basic_info(coin):
        """
        Fetches coin basic info via karpet library.

        :param Coin coin: Coin model instance.
        :return: Basic info as a dict.
        :rtype: dict
        """

        api = Karpet()

        if coin.coin_id:
            return api.get_basic_info(slug=coin.coin_id)

        return api.get_basic_info(symbol=coin.symbol)

    @staticmethod
    def get_dividends(share):
        """
        Fetches share dividends via rug library.

        :param Share share: Share model instance.
        :return: Dividends as a list.
        :rtype: list
        """
        api = TipRanks(share)

        try:
            return api.get_dividends()
        except SymbolNotFound:
            LOGGER.debug(f"Dividends weren't downloaded - symbol {share} wasn't found.")
        except:
            LOGGER.exception(
                "Dividends weren't downloaded",
                extra={"share": share},
            )

        return []

    @staticmethod
    def get_current_price_and_change(item):
        """
        Fetches current market price, market staten and price change in
        value and percents.

        :param Item item: Item model instance to be fetched price for.
        :return: Dataclass with price, state and change values.
        :rtype: CurrentPrice
        """

        def for_share_or_index_or_etf(symbol):
            """
            Note: Yahoo doesn't provide pre/post market prices to indexes.
            """

            price = Yahoo(symbol).get_current_price_change()

            price_key = {
                "pre-market": "pre_market",
                "open": "current_market",
                "closed": "post_market",
                "post-market": "post_market",
            }[price["state"]]

            return CurrentPrice(
                price=float(price[price_key]["value"]),
                state=price["state"],
                change_value=price[price_key]["change"]["value"],
                change_percents=price[price_key]["change"]["percents"],
            )

        def for_coin(coin):
            if coin.coin_id:
                kwargs = {"slug": coin.coin_id}
            else:
                kwargs = {"symbol": coin.symbol}

            data = Karpet().get_basic_info(**kwargs)

            return CurrentPrice(
                price=float(data["current_price"]),
                state="open",
                change_value=data["price_change_24"],
                change_percents=data["price_change_24_percents"],
            )

        if item.is_coin():
            return for_coin(item.coin)

        if item.is_share() or item.is_etf():
            return for_share_or_index_or_etf(item.symbol)

        if item.is_index():
            return for_share_or_index_or_etf(f"^{item.symbol}")

    @staticmethod
    def fetch_price_ratings(share):
        """
        Fetches share price ratings data and directly updates them
        in the database for the given share.

        :param Share share: Share which financials will be downloaded for.
        """

        from .models import Asset

        fv = FinViz(share.symbol)

        try:
            Asset.objects.update_or_create(
                item=share,
                type=Asset.PRICE_RATINGS,
                defaults={"data": fv.get_price_ratings()},
            )
        except SymbolNotFound:
            LOGGER.debug(
                f"Price ratings weren't downloaded - symbol {share} wasn't found."
            )
        except:
            LOGGER.exception(
                "Price ratings weren't downloadeded", extra={"share": share}
            )

    @staticmethod
    def fetch_financials(share):
        """
        Fetches all the share financials data and directly
        updates them in the database for the given share.

        :param Share share: Share which financials will be downloaded for.
        """
        from .models import Asset

        query = AlphaQuery(share.symbol)

        # Revenues.
        try:
            Asset.objects.update_or_create(
                item=share,
                type=Asset.REVENUES_DATA,
                defaults={"data": query.get_revenues()},
            )
            LOGGER.debug(f"Revenues for {share} has been downloaded")
        except SymbolNotFound:
            LOGGER.debug(f"Revenues weren't downloaded - symbol {share} wasn't found.")
        except:
            LOGGER.exception(
                "Revenues weren't downloaded",
                extra={"share": share},
            )

        # Earnings.
        try:
            Asset.objects.update_or_create(
                item=share,
                type=Asset.EARNINGS_DATA,
                defaults={"data": query.get_earnings()},
            )
            LOGGER.debug(f"Earnings for {share} has been downloaded")
        except SymbolNotFound:
            LOGGER.debug(f"Earnings weren't downloaded - symbol {share} wasn't found.")
        except:
            LOGGER.exception(
                "Earnings weren't downloaded",
                extra={"share": share},
            )

        # EPS.
        try:
            Asset.objects.update_or_create(
                item=share, type=Asset.EPS_DATA, defaults={"data": query.get_eps()}
            )
            LOGGER.debug(f"EPS for {share} has been downloaded")
        except SymbolNotFound:
            LOGGER.debug(f"EPS weren't downloaded - symbol {share} wasn't found.")
        except:
            LOGGER.exception(
                "EPS wasn't downloaded",
                extra={"share": share},
            )

    @staticmethod
    def fetch_ratings(share):
        from .models import Asset

        bar = BarChart(share.symbol)

        try:
            Asset.objects.update_or_create(
                item=share,
                type=Asset.RATINGS_DATA,
                defaults={"data": bar.get_ratings()},
            )
        except:
            LOGGER.exception("Ratings weren't downloadeded", extra={"share": share})

    @staticmethod
    def fetch_share_prices(share, history="max"):
        """
        Downloads all prices for the share.
        Returns dataframe with following columns:

        - Date (index)
        - Open
        - High
        - Low
        - Close
        - Volume
        - Dividends
        - Stock Splits

        :param Share share: Share model instance we want prices for.
        :return: Pandas dataframe.
        :rtype: pandas.DataFrame
        """

        try:
            ticker = yf.Ticker(share.symbol)
            df = ticker.history(period=history)

            LOGGER.debug(f"Share prices successfully downloaded for {share}.")
        except:
            LOGGER.exception("Couldn't fetch share prices.")

            return pd.DataFrame()

        return df

    @staticmethod
    def fetch_etf_prices(etf):
        """
        Downloads all prices for the etf.
        Returns dataframe with following columns:

        - Date (index)
        - Open
        - High
        - Low
        - Close
        - Volume
        - Dividends
        - Stock Splits

        :param Etf etf: Etf model instance we want prices for.
        :return: Pandas dataframe.
        :rtype: pandas.DataFrame
        """

        try:
            ticker = yf.Ticker(etf.symbol)
            df = ticker.history("max")

            LOGGER.debug(f"Etf prices successfully downloaded for {etf}.")
        except:
            LOGGER.exception("Couldn't fetch etf prices.")

            return pd.DataFrame()

        return df

    @staticmethod
    def fetch_index_prices(index):
        """
        Downloads all prices for the index.
        Returns dataframe with following columns:

        - Date (index)
        - Open
        - High
        - Low
        - Close

        :param Share share: Share model instance we want prices for.
        :return: Pandas dataframe.
        :rtype: pandas.DataFrame
        """

        try:
            ticker = yf.Ticker(f"^{index.symbol}")
            df = ticker.history("max")
            # Drop 0 value columns.
            df = df.drop(["Volume", "Dividends", "Stock Splits"], axis=1)

            LOGGER.debug(f"Index prices successfully downloaded for {index}.")
        except:
            LOGGER.exception("Couldn't fetch index prices.")

            return pd.DataFrame()

        return df

    @staticmethod
    def fetch_coin_prices(coin):
        """
        Downloads all prices for the coin since settings.COIN_EPOCH.
        Returns dataframe with following columns:

        - date (index)
        - price
        - market_cap
        - total_volume

        :param Coin coin: Coin model instance we want prices for.
        :return: Pandas dataframe.
        :rtype: pandas.DataFrame
        """

        LOGGER.debug(f"Downloading prices for {coin.symbol}.")

        # Try to download historical data.
        try:
            karpet = Karpet(settings.COIN_EPOCH, date.today())

            df = karpet.fetch_crypto_historical_data(coin.symbol, coin.coin_id)

            LOGGER.debug(f"Prices sucessfully downloaded for {coin.symbol}.")
        except:
            LOGGER.exception(f"Couldn't download historical data for {coin.symbol}.")

            return pd.DataFrame()

        # Sort the dataframe.
        df = df.sort_index()

        return df

    @staticmethod
    def fetch_intraday_prices(item):
        """
        Fetches market (intraday) data prices for shares, indexes and ETFs.
        For coins past 24 hours prices are fetched in 30 minutes interval.

        :param Item item: Item model instance we want prices for.
        :return: Pandas dataframe.
        :rtype: pandas.DataFrame
        """

        def for_share_or_index_or_etf(item):
            """
            Downloads all (including pre/post market) prices for the share
            in 5 menut intervals.
            Returns dataframe with following columns:

            - Date (index)
            - Open
            - High
            - Low
            - Close
            - Volume
            - Dividends
            - Stock Splits

            :param Share share: Share model instance we want prices for.
            :return: Pandas dataframe.
            :rtype: pandas.DataFrame
            """

            open = None
            close = None

            try:
                ticker = yf.Ticker(
                    f"^{item.symbol}" if item.is_index() else item.symbol
                )
                df = ticker.history("1d", interval="5m")
                df_pp = ticker.history("1d", interval="5m", prepost=True)

                # Is market open yet?
                if len(df) and df.index[0] > df_pp.index[0]:
                    open = df.index[0]

                # Is market closed yet?
                if len(df) and df.index[-1] < df_pp.index[-1]:
                    close = df.index[-1]

                LOGGER.debug(
                    f"Share intraday prices successfully downloaded for {item.symbol}."
                )
            except:
                LOGGER.exception("Couldn't fetch share intraday prices.")

                return ()

            return df_pp, open, close

        @staticmethod
        def for_coin(coin):
            """
            Fitchis market prices for past 24 hours with 30 minutes interval.
            Returns dataframe with following columns:

            - date time (index)
            - open
            - high
            - low
            - close

            :param Coin coin: Coin modil instance we want prices for.
            :return: Pandas dataframe.
            :rtype: pandas.DataFrame
            """

            k = Karpet()

            if coin.coin_id:
                return k.fetch_crypto_live_data(slug=coin.coin_id)

            return k.fetch_crypto_live_data(symbol=coin.symbol)

        if item.is_coin():
            return for_coin(item.coin), None, None

        if item.is_share() or item.is_index() or item.is_etf():
            return for_share_or_index_or_etf(item)

    @staticmethod
    def fetch_etf_holdings(etf):
        """
        :param Item item: Item model instance we want prices for.
        """

        api = EtfDb(etf.symbol)
        try:
            return api.get_holdings()
        except SymbolNotFound:
            LOGGER.debug(f"Holdings weren't downloaded - symbol {etf} wasn't found.")
        except:
            LOGGER.exception("Holdings weren't downloaded", extra={"etf": etf})

        return {}