Фундаментальные данные по акциям и расчёт F-балла Пиотроски

TL;DR: Я использую Alpha Vantage для вызова API фундаментальных данных за последние 5 лет и выполняю простые манипуляции с использованием панд для расчета F-балла (который дает краткую сводку того, как акции работают фундаментально).

Фондовый рынок действительно является замечательным зверем, пугающе сложным и почти невозможным для прогнозирования. Я искренне верю, что это то, что делает эту область такой захватывающей для меня, так много проблем, которые нужно решить, так много вещей, которые нужно понять, и так много способов дальнейшего развития моих навыков науки о данных! В настоящее время я сочетаю как технический, так и фундаментальный анализ в своих торговых стратегиях, что, по моему мнению, помогает в снижении финансовых рынков.

Я нахожу гораздо более простым тестировать любые торговые стратегии, основанные исключительно на ценовом действии, это из-за богатства бесплатных исторических данных (например, от Yahoo Finance). Однако получение исторических фундаментальных данных бесплатно является более сложной задачей… До сих пор два лучших способа, которые я нашел для получения исторических фундаментальных данных бесплатно:

  • Использование пакета yfinance: Проще говоря, вы можете использовать отличный и очень хорошо документированный пакет для получения фундаментальных данных. Тем не менее, вы ограничены тем, что находится на финансовом веб-сайте Yahoo (последние 4 года или 4 квартала).yfinance
  • Вызов API с помощью Alpha Vantageполучив бесплатный ключ API, вы можете получить последние 5 лет или 20 кварталов фундаментальных данных. Ограничение здесь заключается в том, что вы можете звонить API только 5 раз в минуту и максимум 500 раз в день.

Лично я предпочитаю подход Alpha Vantage, просто потому, что таким образом вы можете получить больше данных и, следовательно, протестировать любые идеи в течение более длительного периода времени. В этой статье будут показаны мои скрипты Python для получения фундаментальных данных из этого источника.

Получение фундаментальных данных с помощью Alpha Vantage

Прежде всего, для загрузки фундаментальных данных необходимо получить бесплатный ключ API, который позволит совершать 500 звонков в день, со скоростью не более 5 звонков в минуту. Получив этот ключ, вы можете использовать и/или изменить следующий код:

import time
from tqdm import tqdm
from alpha_vantage.fundamentaldata import FundamentalData

# Global variables
API_KEY = 'INSERT YOUR API KEY HERE AS A STRING'
CASES_TO_NAMES = {
    'q_BS': 'quarterly_balance_sheet',
    'q_IS': 'quarterly_income_statement',
    'q_CF': 'quarterly_cash_flow',
    'a_BS': 'annual_balance_sheet',
    'a_IS': 'annual_income_statement',
    'a_CF': 'annual_cash_flow',
}
DATA_DIR = ''
TICKERS = ['TSLA', 'AAPL', 'MSFT']


def fundamental_api_call(case: str,
                         ticker: str, 
                         fd):
    '''
    Due to the API limit of 5 calls per minute, this function was desinged so
    that every 5th download can impose a sleep time of a minute.

    Parameters
    ----------
    case : str
        The download switch statement.
    ticker : str
        The ticker to download the data for
    fd : Fundamental Data Downloader
        The fundamental data downloader for AlphaVantage.

    Returns
    -------
    None
    '''
    
    if case == 'a_IS':
        data = fd.get_income_statement_annual(ticker)
    elif case == 'a_BS':
        data = fd.get_balance_sheet_annual(ticker)
    elif case == 'a_CF':
        data = fd.get_cash_flow_annual(ticker)
    elif case == 'q_IS':
        data = fd.get_income_statement_quarterly(ticker)
    elif case == 'q_BS':
        data = fd.get_balance_sheet_quarterly(ticker)
    elif case == 'q_CF':
        data = fd.get_cash_flow_quarterly(ticker)
        
    data[0].to_csv(
        DATA_DIR + ticker + '_' + CASES_TO_NAMES[case] + '.csv',
        index = False
    )
    return


def get_fundamentals(tickers: list):
    '''
    Using the AlphaVantage API, obtain the historical fundamental data for a
    set of tickers

    Parameters
    ----------
    tickers : list
        The list of tickers to download the fundamentals for

    Returns
    -------
    None
    
    Notes
    -----
    On a free API key, only 500 calls per day are allowed. Given that this
    function downloads 6 different statements, a maximum of 83 tickers can be
    considered. Likewise, only 5 API calls can be made per minute, so a sleep
    step is included at every 5 downloads.
    '''
    
    fd = FundamentalData(
        API_KEY,
        output_format = 'pandas',
    )
    
    # This counter allows us to halt after 5 api calls in a single minute
    download = 0
    
    # List to store and print the incomplete downloads
    incomplete_downloads = []
    
    for ticker in tqdm(tickers):
        
        for case in CASES_TO_NAMES.keys():
            
            try:
                fundamental_api_call(case, ticker, fd)
            except Exception as e:
                incomplete_downloads.append(ticker)
                print(ticker, ':', e)
            
            # Check the API limit per minute has not been breached
            download += 1
            if download%5 == 0:
                time.sleep(65)
            
    print('Incompleted downloads :', list(set(incomplete_downloads)))
    return


if __name__ == '__main__':
    get_fundamentals(TICKERS)

Этот код настроен на выполнение следующих действий:

  • Загрузите квартальные и годовые фундаментальные данные за последние пять лет для Tesla, Apple и Microsoft.
  • Поместите загруженные данные в корневой каталог, вы можете изменить это, изменив переменную на путь по вашему выбору.DATA_DIR
  • Делайте 65-секундный перерыв после каждых 5 вызовов API. Я использовал 65 секунд, чтобы быть в безопасности, на случай, если что-то пойдет не так с вызовами скрипта / API.
  • Используйте пакет, чтобы задать индикатор выполнения загрузки.tqdm

Примечание: Учитывая ограничения API, вы можете загружать максимум 83 тикеров данных в день (при условии, что вам нужны как квартальные, так и годовые данные).

F-score Пиотроски и как я его использую

Впервые я узнал о F-балле Пиотроски из видео на YouTube, подготовленного каналом Financial Wisdom Channel (отличный канал для информации о финансовом рынке). По сути, F-оценка представляет собой целое значение от 0 до 9, которое отражает, насколько хорошо компания работает фундаментально (чем выше число, тем лучше).

Мой вариант использования этой оценки заключается в том, чтобы рассматривать ее как часть головоломки при размещении сделки, а не основывать стратегию исключительно на этом. Например, если я нахожу несколько акций, которые имеют хорошие технические настройки (т.е. основаны только на ценовом действии), то F-оценка помогает мне сузить это так, что я выбираю только из корзины фундаментально сильных акций с хорошим потенциалом для роста (хотя в середине 2022 года вряд ли что-то растет!).

Девять критериев для F-балла являются следующими:

  1. Положительная чистая прибыль.
  2. Положительная рентабельность активов.
  3. Положительный операционный денежный поток в текущем периоде.
  4. Денежный поток от операционной деятельности превышает чистую прибыль.
  5. Снижение долгосрочного долга в текущем периоде, по сравнению с прошлым периодом.
  6. Более высокий текущий коэффициент, чем в предыдущем периоде.
  7. Новых акций за последний период выпущено не было.
  8. Более высокая валовая прибыль, чем в предыдущем периоде.
  9. Более высокий коэффициент оборачиваемости активов, чем в предыдущем периоде.

Этот расчет может быть достигнут с помощью кода:

import pandas as pd

# Global variables
TICKERS = ['TSLA', 'MSFT', 'AAPL']
PERIOD = 'annual' # Either 'annual' or 'quarterly'
DATA_LOC = ''


def read_fundamentals(ticker: str, period: str) -> pd.DataFrame:
    '''
    Read in all the fundamentals and merge into a single df
    '''
    
    # Read in the fundamental data
    income_statement = pd.read_csv(
        DATA_LOC + f'{ticker}_{period}_income_statement.csv',
    )
    balance_sheet = pd.read_csv(
        DATA_LOC + f'{ticker}_{period}_balance_sheet.csv',    
    )
    cash_flow = pd.read_csv(
        DATA_LOC + f'{ticker}_{period}_cash_flow.csv'    
    )
    
    # Merge the dataframes together into one larger df
    df = (
        income_statement.merge(
             balance_sheet[
                 get_distinct_cols(income_statement, balance_sheet) + 
                 ['fiscalDateEnding']
             ],
             on = 'fiscalDateEnding',
             how = 'inner'
        )    
    )
    
    df = df.merge(
        cash_flow[get_distinct_cols(df, cash_flow) + ['fiscalDateEnding']],
        on = 'fiscalDateEnding',
        how = 'inner' 
    )
    
    # Clean the dataframe from 'None' strings, and change all numerical cols
    # to floats
    df = df.replace('None', '')
    numeric_cols = [
        col for col in df.columns
        if col not in ['fiscalDateEnding', 'reportedCurrency']
    ]
    df[numeric_cols] = df[numeric_cols].apply(pd.to_numeric)
    
    return df.sort_values('fiscalDateEnding', ascending = False)
    

def get_f_score(ticker: str, period: str) -> int:
    '''
    Get the a binary dataframe indicating which criteria are filled in the
    Piotorski F-score for each date in the fundamental dataset
    
    Notes
    -----
    Strategy taken from here:
    https://www.investopedia.com/terms/p/piotroski-score.asp
    
    Some calculations taken from here:
    https://www.quant-investing.com/blog/this-academic-can-help-you-make-better-investment-decisions-piotroski-f-score
    '''
    
    df = read_fundamentals(ticker, period)

    # Parameters requried for the derivation of the F-score
    return_on_assets = df['netIncome']/df['totalAssets']
    current_ratio = df['totalCurrentAssets']/df['totalCurrentLiabilities']
    new_shares = (
        df['commonStockSharesOutstanding']
        - df['commonStockSharesOutstanding'].shift(-1)
    )
    gross_margin = df['totalRevenue'] - df['costofGoodsAndServicesSold']
    average_assets = (df['totalAssets'] + df['totalAssets'].shift(-1))/2
    asset_turnover_ratio = df['totalRevenue']/average_assets
    
    return (
        int(df['netIncome'].values[0] > 0) + 
        int(return_on_assets.values[0] > 0) + 
        int(df['operatingCashflow'].values[0] > 0) + 
        int(df['operatingCashflow'].values[0] > df['netIncome'].values[0]) + 
        int(df['longTermDebt'].values[0] < df['longTermDebt'].values[1]) +
        int(current_ratio.values[0] > current_ratio.values[1]) + 
        int(new_shares.values[0] <= 0) + 
        int(gross_margin.values[0] > gross_margin.values[1]) + 
        int(asset_turnover_ratio.values[0] > asset_turnover_ratio.values[1])
    )


def get_distinct_cols(df1: pd.DataFrame, df2: pd.DataFrame) -> list:
    '''
    Return the col names that are in df2 which are not in df1
    '''
    return [col for col in df2.columns if col not in df1.columns]


if __name__ == '__main__':
    for ticker in TICKERS:
        f_score = get_f_score(ticker, PERIOD)
        
        print(f'{ticker} {PERIOD} F-score : {f_score}')

Обратите внимание, что функция дает только самую последнюю F-оценку, эта функция может быть изменена для получения F-балла за каждый квартал / год (кроме последнего, потому что мы требуем период до этого для вычисления F-балла).get_f_score

Источник