summaryrefslogtreecommitdiffstats
path: root/openbb_platform/providers/fmp/openbb_fmp/models/equity_screener.py
blob: beb23b807853f0da99fb86f985ac5bca08af19aa (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
"""FMP Equity Screener Model."""

# pylint: disable=unused-argument
from copy import deepcopy
from typing import Any, Dict, List, Literal, Optional

import pandas as pd
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.standard_models.equity_screener import (
    EquityScreenerData,
    EquityScreenerQueryParams,
)
from openbb_core.provider.utils.errors import EmptyDataError
from openbb_fmp.utils.definitions import EXCHANGES, SECTORS
from openbb_fmp.utils.helpers import create_url, get_data
from pydantic import Field


class FMPEquityScreenerQueryParams(EquityScreenerQueryParams):
    """FMP Equity Screener Query."""

    __alias_dict__ = {
        "mktcap_min": "marketCapMoreThan",
        "mktcap_max": "marketCapLowerThan",
        "price_min": "priceMoreThan",
        "price_max": "priceLowerThan",
        "beta_min": "betaMoreThan",
        "beta_max": "betaLowerThan",
        "volume_min": "volumeMoreThan",
        "volume_max": "volumeLowerThan",
        "dividend_min": "dividendMoreThan",
        "dividend_max": "dividendLowerThan",
        "is_active": "isActivelyTrading",
        "is_etf": "isEtf",
    }

    mktcap_min: Optional[int] = Field(
        default=None, description="Filter by market cap greater than this value."
    )
    mktcap_max: Optional[int] = Field(
        default=None,
        description="Filter by market cap less than this value.",
    )
    price_min: Optional[float] = Field(
        default=None,
        description="Filter by price greater than this value.",
    )
    price_max: Optional[float] = Field(
        default=None,
        description="Filter by price less than this value.",
    )
    beta_min: Optional[float] = Field(
        default=None,
        description="Filter by a beta greater than this value.",
    )
    beta_max: Optional[float] = Field(
        default=None,
        description="Filter by a beta less than this value.",
    )
    volume_min: Optional[int] = Field(
        default=None,
        description="Filter by volume greater than this value.",
    )
    volume_max: Optional[int] = Field(
        default=None,
        description="Filter by volume less than this value.",
    )
    dividend_min: Optional[float] = Field(
        default=None,
        description="Filter by dividend amount greater than this value.",
    )
    dividend_max: Optional[float] = Field(
        default=None,
        description="Filter by dividend amount less than this value.",
    )
    is_etf: Optional[bool] = Field(
        default=False,
        description="If true, returns only ETFs.",
    )
    is_active: Optional[bool] = Field(
        default=True,
        description="If false, returns only inactive tickers.",
    )
    sector: Optional[SECTORS] = Field(default=None, description="Filter by sector.")
    industry: Optional[str] = Field(default=None, description="Filter by industry.")
    country: Optional[str] = Field(
        default=None, description="Filter by country, as a two-letter country code."
    )
    exchange: Optional[EXCHANGES] = Field(
        default=None, description="Filter by exchange."
    )
    limit: Optional[int] = Field(
        default=50000, description="Limit the number of results to return."
    )


class FMPEquityScreenerData(EquityScreenerData):
    """FMP Equity Screener Data."""

    __alias_dict__ = {
        "name": "companyName",
    }

    market_cap: Optional[int] = Field(
        description="The market cap of ticker.", alias="marketCap", default=None
    )
    sector: Optional[str] = Field(
        description="The sector the ticker belongs to.", default=None
    )
    industry: Optional[str] = Field(
        description="The industry ticker belongs to.", default=None
    )
    beta: Optional[float] = Field(description="The beta of the ETF.", default=None)
    price: Optional[float] = Field(description="The current price.", default=None)
    last_annual_dividend: Optional[float] = Field(
        description="The last annual amount dividend paid.",
        alias="lastAnnualDividend",
        default=None,
    )
    volume: Optional[int] = Field(
        description="The current trading volume.", default=None
    )
    exchange: Optional[str] = Field(
        description="The exchange code the asset trades on.",
        alias="exchangeShortName",
        default=None,
    )
    exchange_name: Optional[str] = Field(
        description="The full name of the primary exchange.",
        alias="exchange",
        default=None,
    )
    country: Optional[str] = Field(
        description="The two-letter country abbreviation where the head office is located.",
        default=None,
    )
    is_etf: Optional[Literal[True, False]] = Field(
        description="Whether the ticker is an ETF.", alias="isEtf", default=None
    )
    actively_trading: Optional[Literal[True, False]] = Field(
        description="Whether the ETF is actively trading.",
        alias="isActivelyTrading",
        default=None,
    )


class FMPEquityScreenerFetcher(
    Fetcher[
        FMPEquityScreenerQueryParams,
        List[FMPEquityScreenerData],
    ]
):
    """Transform the query, extract and transform the data from the FMP endpoints."""

    @staticmethod
    def transform_query(params: Dict[str, Any]) -> FMPEquityScreenerQueryParams:
        """Transform the query."""
        return FMPEquityScreenerQueryParams(**params)

    @staticmethod
    async def aextract_data(
        query: FMPEquityScreenerQueryParams,
        credentials: Optional[Dict[str, str]],
        **kwargs: Any,
    ) -> List[Dict]:
        """Return the raw data from the FMP endpoint."""
        api_key = credentials.get("fmp_api_key") if credentials else ""
        _query = deepcopy(query)
        if _query.sector is not None:
            _query.sector = _query.sector.replace("_", " ").title()
        url = create_url(
            version=3,
            endpoint="stock-screener",
            api_key=api_key,
            query=_query,
            exclude=["query", "is_symbol", "industry"],
        ).replace(" ", "%20")
        return await get_data(url, **kwargs)  # type: ignore

    @staticmethod
    def transform_data(
        query: FMPEquityScreenerQueryParams, data: List[Dict], **kwargs: Any
    ) -> List[FMPEquityScreenerData]:
        """Return the transformed data."""
        if not data:
            raise EmptyDataError("The request was returned empty.")
        results = pd.DataFrame(data)
        if query.industry:
            results = results[
                results["sector"].str.contains(query.industry, case=False)
                | results["industry"].str.contains(query.industry, case=False)
            ]
        results["companyName"] = results["companyName"].fillna("-").replace("-", "")
        for col in results:
            if results[col].dtype in ("int", "float"):
                results[col] = results[col].fillna(0).replace(0, None)
        return [
            FMPEquityScreenerData.model_validate(d)
            for d in results.sort_values(by="marketCap", ascending=False).to_dict(
                "records"
            )
        ]