Библиотека yfinance и API Yahoo Finance: методы загрузки данных, фильтры, параметры

За годы работы с финансовыми данными я перепробовал десятки различных биржевых API. Однако когда речь заходит о быстром доступе к качественным историческим данным без лишних заморочек и долгих настроек, yfinance остается одним из самых надежных инструментов в арсенале квант-аналитика.

Эта библиотека, построенная на основе API Yahoo Finance, предоставляет программный доступ к огромному массиву финансовой информации, который многие профессионалы индустрии используют для бэктестинга стратегий и исследовательских задач. В этой статье мы рассмотрим нюансы использования yfinance, базовые методы загрузки данных, а также продвинутые техники работы с API, оптимизацию запросов и обработку особых случаев, с которыми сталкиваются профессиональные трейдеры и исследователи.

Архитектура и принципы работы yfinance

Начнем с главного: yfinance не является официальным продуктом Yahoo. Данная библиотека представляет собой Python-обертку, эксплуатирующую инфраструктуру Yahoo Finance: данные ее сайта, публичных страниц и эндпоинтов.

В отличие от официальных API, которые требуют регистрации и часто ограничивают количество запросов, yfinance работает через HTTP-запросы к тем же эндпоинтам, которые использует веб-интерфейс Yahoo Finance. Это дает нам несколько важных преимуществ:

  1. отсутствие rate limits в традиционном понимании;
  2. бесплатный доступ;
  3. относительно высокую скорость получения данных.

Внутренняя архитектура yfinance построена вокруг класса Ticker, который служит основным интерфейсом для взаимодействия с данными конкретного финансового инструмента. Каждый экземпляр Ticker инкапсулирует логику работы с различными типами данных — от исторических цен до фундаментальных показателей.

Важно понимать, что библиотека использует ленивую загрузку данных: информация запрашивается с серверов Yahoo только при первом обращении к соответствующему атрибуту или методу.

Получить данные о котировках акций или индексов с помощью yfinance можно буквально в пару строк кода:

import yfinance as yf
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
pd.set_option('display.expand_frame_repr', False)

# Создание объекта Ticker - это легковесная операция
ticker = yf.Ticker("AAPL")

# Данные загружаются только при первом обращении
hist_data = ticker.history(period="1y")
print(hist_data)
                                 Open        High         Low       Close     Volume  Dividends  Stock Splits
Date                                                                                                         
2024-06-12 00:00:00-04:00  206.404722  219.175002  205.936908  212.078201  198134300        0.0           0.0
2024-06-13 00:00:00-04:00  213.740409  215.741048  210.615026  213.242737   97862700        0.0           0.0
2024-06-14 00:00:00-04:00  212.854540  214.168387  210.316407  211.500870   70122700        0.0           0.0
2024-06-17 00:00:00-04:00  212.376781  217.930808  211.729813  215.661423   93728300        0.0           0.0
2024-06-18 00:00:00-04:00  216.577122  217.612289  212.008492  213.292480   79943300        0.0           0.0
...                               ...         ...         ...         ...        ...        ...           ...
2025-06-05 00:00:00-04:00  203.500000  204.750000  200.149994  200.630005   55126100        0.0           0.0
2025-06-06 00:00:00-04:00  203.000000  205.699997  202.050003  203.919998   46607700        0.0           0.0
2025-06-09 00:00:00-04:00  204.389999  206.000000  200.020004  201.449997   72862600        0.0           0.0
2025-06-10 00:00:00-04:00  200.600006  204.350006  200.570007  202.669998   54672600        0.0           0.0
2025-06-11 00:00:00-04:00  203.500000  204.500000  198.410004  198.779999   60820200        0.0           0.0

[250 rows x 7 columns]

Что удобно — таблицы, которые отдают yfinance являются обычными Pandas датафреймами. Их можно с минимумом усилий анализировать инструментами Pandas, либо строить визуализации с помощью Matplotlib, Plotly, Mpfinance и т. д.

hist_data['Close'].plot()

График цен закрытия акций Apple за последние 12 месяцев

Рис. 1: График цен закрытия акций Apple за последние 12 месяцев

Даты и время

По умолчанию время в столбце Date обычно указано либо UTC (Гринвич), либо по Нью-Йорку. Это легко проверить, обратившись к методу tz (timezone):

# Проверим какой у индекса часовой пояс
print(hist_data.index.tz)
America/New_York

Если необходимо конвертировать время из America/New_York в локальное, например Moscow (Europe/Moscow), то можно использовать метод tz_convert().

# Конвертируем в московское время
hist_data.index = hist_data.index.tz_convert("Europe/Moscow")
print(hist_data.tail())
                                 Open        High         Low       Close    Volume  Dividends  Stock Splits
Date                                                                                                        
2025-06-05 07:00:00+03:00  203.500000  204.750000  200.149994  200.630005  55126100        0.0           0.0
2025-06-06 07:00:00+03:00  203.000000  205.699997  202.050003  203.919998  46607700        0.0           0.0
2025-06-09 07:00:00+03:00  204.389999  206.000000  200.020004  201.449997  72862600        0.0           0.0
2025-06-10 07:00:00+03:00  200.600006  204.350006  200.570007  202.669998  54672600        0.0           0.0
2025-06-11 07:00:00+03:00  203.500000  204.500000  198.410004  198.779999  60820200        0.0           0.0

Далее мы уже можем форматировать дату и время как душе будет угодно. Если вы находите что столбец Date слишком длинный и плохо читаемый, то можно явно прописать свой формат в питоновской функции strftime.

# Конвертируем индекс времени в нужный строковый формат
hist_data.index = hist_data.index.strftime("%d.%m.%y %H:%M")
print(hist_data.tail())
                      Open        High         Low       Close    Volume  Dividends  Stock Splits
Date                                                                                             
05.06.25 07:00  203.500000  204.750000  200.149994  200.630005  55126100        0.0           0.0
06.06.25 07:00  203.000000  205.699997  202.050003  203.919998  46607700        0.0           0.0
09.06.25 07:00  204.389999  206.000000  200.020004  201.449997  72862600        0.0           0.0
10.06.25 07:00  200.600006  204.350006  200.570007  202.669998  54672600        0.0           0.0
11.06.25 07:00  203.500000  204.500000  198.410004  198.779999  60820200        0.0           0.0

Для акций и дневных котировок в столбце Date время будет всегда одинаковым. Если же это intraday данные актива, который торгуется 24/7, то вы получите часы и минуты с лагом в 1 таймфрейм с вашим текущим временем. Вот как это выглядит на 5-минутных данных, когда у меня на часах 16:02.

# Загружаем 5-минутные данные за последний торговый день
data = yf.download(
    tickers="BTC-USD", #Bitcoin
    period="1d",     # Данные за 1 день (можно поставить "7d" для недели)
    interval="5m",   # 5-минутный интервал
    progress=False   # Отключаем прогресс-бар
)

# Конвертируем в московское время
data.index = data.index.tz_convert("Europe/Moscow")

# Конвертируем индекс времени в нужный строковый формат
data.index = data.index.strftime("%d.%m.%y %H:%M")  
print(data.tail())
Price                   Close           High            Low           Open     Volume
Ticker                BTC-USD        BTC-USD        BTC-USD        BTC-USD    BTC-USD
Datetime                                                                             
12.06.25 15:40  107359.765625  107359.765625  107266.960938  107266.960938  146354176
12.06.25 15:45  107314.421875  107398.250000  107314.421875  107398.250000  127410176
12.06.25 15:50  107153.656250  107292.148438  107153.656250  107292.148438  975474688
12.06.25 15:55  106977.468750  107082.390625  106977.468750  107082.390625  103247872
12.06.25 16:00  107000.312500  107000.312500  107000.312500  107000.312500          0

Кэширование

Механизм кэширования в yfinance заслуживает особого внимания. При повторных запросах одних и тех же данных библиотека может использовать кэшированные результаты, что существенно ускоряет работу в интерактивных сессиях. Однако в production-среде это может привести к получению устаревших данных, поэтому рекомендую явно управлять кэшем или использовать fresh-запросы для критически важных приложений.

Обработка сетевых запросов и error handling

Одним из ключевых аспектов профессиональной работы с yfinance является понимание того, как библиотека обрабатывает сетевые запросы и ошибки.

Yahoo Finance периодически возвращает HTTP-ошибки, неполные данные или неожиданные форматы ответов. В своей практике я сталкивался с ситуациями, когда запрос данных за определенный период возвращал частичные результаты или данные с пропусками, особенно для неликвидных биржевых инструментов. Поэтому я иногда использую такую обработку запросов:

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

def create_robust_session():
    """Создание сессии с retry-логикой для yfinance"""
    session = requests.Session()
    
    retry_strategy = Retry(
        total=3,
        backoff_factor=1,
        status_forcelist=[429, 500, 502, 503, 504],
    )
    
    adapter = HTTPAdapter(max_retries=retry_strategy)
    session.mount("http://", adapter)
    session.mount("https://", adapter)
    
    return session

# Применение кастомной сессии к yfinance
def get_data_with_retry(symbol, **kwargs):
    """Получение данных с дополнительной обработкой ошибок"""
    session = create_robust_session()
    
    try:
        ticker = yf.Ticker(symbol, session=session)
        data = ticker.history(**kwargs)
        
        # Проверка на пустые данные
        if data.empty:
            raise ValueError(f"Нет данных для символа {symbol}")
            
        return data
        
    except Exception as e:
        print(f"Ошибка при получении данных для {symbol}: {e}")
        return pd.DataFrame()

Этот подход особенно важен при работе с batch-обработкой большого количества символов (тикеров), где отказ одного запроса не должен останавливать весь процесс. В production-системах я рекомендую реализовывать exponential backoff и circuit breaker паттерны для обеспечения стабильности работы.

Методы загрузки исторических данных

Базовые методы и их параметры

Метод history() является основным инструментом для получения исторических данных о ценах и объемах торгов. Однако его возможности выходят далеко за рамки простого получения OHLCV-данных. Параметры этого метода позволяют тонко настраивать временные интервалы, типы данных и способы их обработки.

Ключевые параметры метода history():

Параметр Тип Описание Примеры значений
period str Период данных «1d», «5d», «1mo», «3mo», «6mo», «1y», «2y», «5y», «10y», «ytd», «max»
interval str Интервал данных «1m», «2m», «5m», «15m», «30m», «60m», «90m», «1h», «1d», «5d», «1wk», «1mo», «3mo»
start str/datetime Дата начала «2023-01-01»
end str/datetime Дата окончания «2023-12-31»
prepost bool Включить пре/пост-маркет True/False
auto_adjust bool Автокорректировка на дивиденды True (по умолчанию)
back_adjust bool Корректировка назад False (по умолчанию)
repair bool Автоисправление данных False (по умолчанию)
keepna bool Сохранять пропуски False (по умолчанию)

Ниже пример кода простой загрузки котировок Apple сразу по нескольким таймфреймам.

# Различные способы задания временного периода
apple = yf.Ticker("AAPL")

# Предустановленные периоды
data_1y = apple.history(period="1y")
data_5y = apple.history(period="5y")
data_max = apple.history(period="max")

# Кастомные даты
from datetime import datetime
start_date = datetime(2024, 6, 1)
end_date = datetime(2025, 6, 11)
data_custom = apple.history(start=start_date, end=end_date)

# Различные интервалы данных
data_1m = apple.history(period="1d", interval="1m")  # Минутные данные
data_5m = apple.history(period="5d", interval="5m")  # 5-минутные данные
data_1h = apple.history(period="1mo", interval="1h") # Часовые данные
data_1d = apple.history(period="1y", interval="1d")  # Дневные данные

print(data_1m.tail())
print(data_5m.tail())
print(data_1h.tail())
print(data_1d.tail())
                                 Open        High         Low       Close   Volume  Dividends  Stock Splits
Datetime                                                                                                   
2025-06-11 15:55:00-04:00  198.580002  198.639999  198.429993  198.464996   407544        0.0           0.0
2025-06-11 15:56:00-04:00  198.490005  198.570007  198.410004  198.539993   367611        0.0           0.0
2025-06-11 15:57:00-04:00  198.520004  198.699997  198.509995  198.639999   379720        0.0           0.0
2025-06-11 15:58:00-04:00  198.695007  198.710007  198.570007  198.695007   442730        0.0           0.0
2025-06-11 15:59:00-04:00  198.690002  198.869995  198.664993  198.850006  1076039        0.0           0.0
                                 Open        High         Low       Close   Volume  Dividends  Stock Splits
Datetime                                                                                                   
2025-06-11 15:35:00-04:00  198.750000  198.899994  198.600006  198.800003   542315        0.0           0.0
2025-06-11 15:40:00-04:00  198.800003  199.069901  198.750000  198.960007   450111        0.0           0.0
2025-06-11 15:45:00-04:00  198.960007  199.059998  198.740005  198.809998   554367        0.0           0.0
2025-06-11 15:50:00-04:00  198.809998  198.940002  198.445007  198.570007  1348308        0.0           0.0
2025-06-11 15:55:00-04:00  198.580002  198.869995  198.410004  198.850006  2673644        0.0           0.0
                                 Open        High         Low       Close    Volume  Dividends  Stock Splits
Datetime                                                                                                    
2025-06-11 11:30:00-04:00  200.959900  201.149994  199.659607  200.207108  10056323        0.0           0.0
2025-06-11 12:30:00-04:00  200.190704  201.236496  200.184998  200.910004   5829259        0.0           0.0
2025-06-11 13:30:00-04:00  200.904999  200.985001  198.710007  199.239197   6328313        0.0           0.0
2025-06-11 14:30:00-04:00  199.220001  199.600006  198.619995  198.979996   6509313        0.0           0.0
2025-06-11 15:30:00-04:00  198.949997  199.125000  198.410004  198.850006   6079089        0.0           0.0
                                 Open        High         Low       Close    Volume  Dividends  Stock Splits
Date                                                                                                        
2025-06-05 00:00:00-04:00  203.500000  204.750000  200.149994  200.630005  55126100        0.0           0.0
2025-06-06 00:00:00-04:00  203.000000  205.699997  202.050003  203.919998  46607700        0.0           0.0
2025-06-09 00:00:00-04:00  204.389999  206.000000  200.020004  201.449997  72862600        0.0           0.0
2025-06-10 00:00:00-04:00  200.600006  204.350006  200.570007  202.669998  54672600        0.0           0.0
2025-06-11 00:00:00-04:00  203.500000  204.500000  198.410004  198.779999  60820200        0.0           0.0

Важно понимать ограничения различных интервалов:

  • минутные данные доступны только за последние 7 дней;
  • 2-минутные, 5-минутные, 15-минутные, 30-минутные — за последние 60 дней;
  • часовые — за последние 730 дней (2 года);
  • дневные данные могут охватывать всю историю торгов инструмента.
Читайте также:  Что такое алгоритмическая торговля и как она работает?

Эти ограничения связаны с политикой Yahoo Finance и не могут быть обойдены программными средствами.

Программный интерфейс API yfinance организован вокруг нескольких ключевых классов, каждый из которых предоставляет специфический функционал:

Метод Описание Тип возвращаемых данных
history() Исторические OHLCV данные DataFrame
info Фундаментальная информация Dict
dividends История дивидендов Series
splits История сплитов акций Series
recommendations Рекомендации аналитиков DataFrame
calendar Календарь событий DataFrame
news Новости компании List[Dict]

Важная особенность: при массовой загрузке данные возвращаются в MultiIndex формате, где первый уровень — это символ тикера, второй — тип данных (Open, High, Low, Close, Volume).

Продвинутые параметры и их влияние на качество данных

Параметр auto_adjust заслуживает особого внимания в контексте профессионального количественного анализа. По умолчанию он установлен в True, что означает автоматическую корректировку исторических цен на дивиденды и сплиты. Для большинства задач бэктестинга это правильное поведение, но в некоторых случаях может потребоваться работа с raw данными.

# Получение данных с различными настройками корректировки
tesla = yf.Ticker("CT=F")  #Cotton Futures

# Автоматически скорректированные данные (по умолчанию)
adjusted_data = tesla.history(period="2y", auto_adjust=True)

# Сырые данные без корректировок
raw_data = tesla.history(period="2y", auto_adjust=False)

# Сравнение влияния корректировок
print("Скорректированная цена закрытия (последняя):", adjusted_data['Close'].iloc[-1])
print("Сырая цена закрытия (последняя):", raw_data['Close'].iloc[-1])
print("Разница:", abs(adjusted_data['Close'].iloc[-1] - raw_data['Close'].iloc[-1]))
Скорректированная цена закрытия (последняя): 65.30000305175781
Сырая цена закрытия (последняя): 65.30000305175781
Разница: 0.0

Параметр prepost позволяет включать данные pre-market и after-hours торгов, что может быть критично для анализа реакции рынка на новости или корпоративные события. Однако следует учитывать, что объемы торгов в эти периоды существенно ниже, что может исказить некоторые технические индикаторы.

# Получение данных с включением pre/post-market торгов
extended_data = tesla.history(
    period="5d", 
    interval="1m", 
    prepost=True,
    auto_adjust=True
)

# Анализ распределения объемов по времени
extended_data['hour'] = extended_data.index.hour
volume_by_hour = extended_data.groupby('hour')['Volume'].mean()

print("Средний объем торгов по часам:")
for hour, volume in volume_by_hour.items():
    print(f"{hour:02d}:00 - {volume:,.0f}")
Средний объем торгов по часам:
01:00 - 3
02:00 - 1
03:00 - 4
04:00 - 1
07:00 - 2
08:00 - 12
09:00 - 17
10:00 - 18
11:00 - 26
13:00 - 0
21:00 - 0
23:00 - 0

Обработка корпоративных событий

Одной из сложностей работы с историческими данными является корректная обработка корпоративных событий: дивидендов, сплитов акций и других корпоративных действий. Библиотека yfinance предоставляет отдельные методы для получения информации об этих событиях, что позволяет более точно анализировать их влияние на торговые стратегии.

# Получение информации о корпоративных действиях
msft = yf.Ticker("MSFT")

# Дивиденды
dividends = msft.dividends
print("Последние 5 дивидендных выплат:")
print(dividends.tail())

# Сплиты акций
splits = msft.splits
print("\nСплиты акций:")
print(splits)

# Анализ дивидендной доходности
hist_data = msft.history(period="5y")
div_yield = dividends.resample('Y').sum() / hist_data['Close'].resample('Y').last() * 100
print("\nДивидендная доходность по годам:")
print(div_yield)
Последние 5 дивидендных выплат:
Date
2024-05-15 00:00:00-04:00    0.75
2024-08-15 00:00:00-04:00    0.75
2024-11-21 00:00:00-05:00    0.83
2025-02-20 00:00:00-05:00    0.83
2025-05-15 00:00:00-04:00    0.83
Name: Dividends, dtype: float64

Сплиты акций:
Date
1987-09-21 00:00:00-04:00    2.0
1990-04-16 00:00:00-04:00    2.0
1991-06-27 00:00:00-04:00    1.5
1992-06-15 00:00:00-04:00    1.5
1994-05-23 00:00:00-04:00    2.0
1996-12-09 00:00:00-05:00    2.0
1998-02-23 00:00:00-05:00    2.0
1999-03-29 00:00:00-05:00    2.0
2003-02-18 00:00:00-05:00    2.0
Name: Stock Splits, dtype: float64

Дивидендная доходность по годам:
Date
2015-12-31 00:00:00-05:00         NaN
2016-12-31 00:00:00-05:00         NaN
2017-12-31 00:00:00-05:00         NaN
2018-12-31 00:00:00-05:00         NaN
2019-12-31 00:00:00-05:00         NaN
2020-12-31 00:00:00-05:00    0.975852
2021-12-31 00:00:00-05:00    0.704306
2022-12-31 00:00:00-05:00    1.080648
2023-12-31 00:00:00-05:00    0.750364
2024-12-31 00:00:00-05:00    0.733533
2025-12-31 00:00:00-05:00    0.351234
Freq: YE-DEC, dtype: float64

При разработке торговых стратегий важно учитывать, что корпоративные события могут создавать искусственные сигналы в технических индикаторах. Например, крупный дивиденд может привести к резкому падению цены акции в ex-dividend date, что может быть ошибочно интерпретировано как сигнал к продаже.

Работа с фундаментальными данными

Помимо исторических цен, yfinance предоставляет доступ к широкому спектру фундаментальной информации о компаниях. Эти данные будут полезны для фундаментального анализа и stock-скрининга.

Основные информационные атрибуты:

Атрибут Тип данных Описание
.info Dict Основная информация о компании
.major_holders DataFrame Крупнейшие держатели акций
.institutional_holders DataFrame Институциональные инвесторы
.recommendations DataFrame Рекомендации аналитиков
.calendar DataFrame Календарь событий
.earnings DataFrame История прибыли
.quarterly_earnings DataFrame Квартальная прибыль
.financials DataFrame Финансовая отчетность
.balance_sheet DataFrame Баланс
.cashflow DataFrame Денежные потоки

Ниже пример запроса фундаментальных данных по акции Microsoft:

# Получение фундаментальной информации
msft = yf.Ticker("MSFT")

# Базовая информация
info = msft.info
print("Название компании:", info.get('longName'))
print("Сектор:", info.get('sector'))
print("Капитализация:", f"${info.get('marketCap', 0):,}")
print("P/E:", info.get('trailingPE'))

# Финансовые показатели
financials = msft.financials
if not financials.empty:
    revenue = financials.loc['Total Revenue'].iloc[0]
    print(f"Последний годовой доход: ${revenue:,.0f}")

# Рекомендации аналитиков
recommendations = msft.recommendations
if recommendations is not None:
    latest_rec = recommendations.tail(1)
    print("\nПоследние рекомендации аналитиков:")
    print(latest_rec[['strongBuy', 'buy', 'hold', 'sell', 'strongSell']].iloc[0])

Фильтрация и обработка данных

Методы очистки данных и обработки аномалий

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

Проблема Причина Решение
Пропущенные значения Нерабочие дни, технические сбои Forward fill, интерполяция
Нулевые объемы Отсутствие торгов Замена на минимальные значения
Экстремальные цены Ошибки данных, микроструктурный шум Z-score фильтрация
Нарушение OHLC логики High < Low, Close > High Логическая корректировка
Временные пропуски Праздники, остановки торгов Календарная синхронизация

Ниже пример кода как можно обнаружить проблемы с данными и исправить их:

import yfinance as yf
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
from scipy import stats

pd.set_option('display.expand_frame_repr', False)

def clean_financial_data(df, z_threshold=4):
    """
    Очистка финансовых данных от аномалий и пропусков
    """
    df_clean = df.copy()
    
    # Обработка пропущенных значений
    for col in ['Open', 'High', 'Low', 'Close']:
        # Forward fill для цен
        df_clean[col] = df_clean[col].ffill()
        
        # Интерполяция для небольших пропусков
        mask = df_clean[col].isna()
        if mask.any():
            df_clean[col] = df_clean[col].interpolate(method='linear')
    
    # Обработка объемов
    df_clean['Volume'] = df_clean['Volume'].fillna(0)
    
    # Выявление аномальных значений по Z-score
    price_cols = ['Open', 'High', 'Low', 'Close']
    returns = df_clean[price_cols].pct_change().dropna()
    
    for col in price_cols:
        ret_col = returns[col]
        z_scores = np.abs(stats.zscore(ret_col.dropna()))
        anomalies = z_scores > z_threshold
        
        if anomalies.any():
            print(f"Обнаружено {anomalies.sum()} аномалий в колонке {col}")
            # Замена аномальных значений медианой
            median_return = ret_col.median()
            anomaly_indices = ret_col.index[anomalies]
            
            for idx in anomaly_indices:
                idx_position = df_clean.index.get_loc(idx)
                if idx_position > 0:
                    prev_idx = df_clean.index[idx_position - 1]
                    prev_price = df_clean.loc[prev_idx, col]
                    df_clean.loc[idx, col] = prev_price * (1 + median_return)
                else:
                    # Для первого элемента используем медианную доходность от второго элемента
                    if len(df_clean) > 1:
                        next_idx = df_clean.index[1]
                        next_price = df_clean.loc[next_idx, col]
                        df_clean.loc[idx, col] = next_price / (1 + median_return)
    
    # Проверка консистентности OHLC
    inconsistent = (
        (df_clean['High'] < df_clean['Low']) |
        (df_clean['High'] < df_clean['Open']) |
        (df_clean['High'] < df_clean['Close']) | (df_clean['Low'] > df_clean['Open']) |
        (df_clean['Low'] > df_clean['Close'])
    )
    
    if inconsistent.any():
        print(f"Обнаружено {inconsistent.sum()} строк с нарушением OHLC логики")
        # Корректировка нарушений
        for idx in df_clean.index[inconsistent]:
            ohlc = df_clean.loc[idx, ['Open', 'High', 'Low', 'Close']]
            df_clean.loc[idx, 'High'] = ohlc.max()
            df_clean.loc[idx, 'Low'] = ohlc.min()
    
    return df_clean

# Пример использования
if __name__ == "__main__":
    spy = yf.Ticker("SPY")
    raw_data = spy.history(period="2y")
    clean_data = clean_financial_data(raw_data)
    
    print("Статистика до очистки:")
    print(raw_data.describe())
    print("\nСтатистика после очистки:")
    print(clean_data.describe())
    
    # Дополнительная проверка качества данных
    print(f"\nПропущенные значения в исходных данных:")
    print(raw_data.isnull().sum())
    print(f"\nПропущенные значения после очистки:")
    print(clean_data.isnull().sum())
Обнаружено 5 аномалий в колонке Open
Обнаружено 4 аномалий в колонке High
Обнаружено 2 аномалий в колонке Low
Обнаружено 4 аномалий в колонке Close
Обнаружено 4 строк с нарушением OHLC логики
Статистика до очистки:
             Open        High         Low       Close        Volume   Dividends  Stock Splits  Capital Gains
count  502.000000  502.000000  502.000000  502.000000  5.020000e+02  502.000000         502.0          502.0
mean   516.372120  519.028993  513.502769  516.504128  6.588117e+07    0.027667           0.0            0.0
std     61.373118   61.643804   60.833385   61.308112  2.661899e+07    0.218257           0.0            0.0
min    405.453822  406.473426  401.189061  402.630249  2.604870e+07    0.000000           0.0            0.0
25%    450.032551  452.066861  448.802123  451.618332  4.694642e+07    0.000000           0.0            0.0
50%    522.962308  525.424988  519.630005  523.161499  6.358180e+07    0.000000           0.0            0.0
75%    569.440058  571.690343  566.192972  569.176361  7.711415e+07    0.000000           0.0            0.0
max    609.705872  611.390763  607.731787  611.091675  2.566114e+08    1.966000           0.0            0.0

Статистика после очистки:
             Open        High         Low       Close        Volume   Dividends  Stock Splits  Capital Gains
count  502.000000  502.000000  502.000000  502.000000  5.020000e+02  502.000000         502.0          502.0
mean   516.547350  519.199882  513.561406  516.524647  6.588117e+07    0.027667           0.0            0.0
std     61.354805   61.715668   60.822639   61.371538  2.661899e+07    0.218257           0.0            0.0
min    405.453822  406.473426  401.189061  402.630249  2.604870e+07    0.000000           0.0            0.0
25%    450.032551  452.066861  448.802123  451.618332  4.694642e+07    0.000000           0.0            0.0
50%    523.637917  526.338001  520.164219  523.136810  6.358180e+07    0.000000           0.0            0.0
75%    569.440058  571.690343  566.192972  569.176361  7.711415e+07    0.000000           0.0            0.0
max    609.705872  611.390763  607.731787  611.091675  2.566114e+08    1.966000           0.0            0.0

Пропущенные значения в исходных данных:
Open             0
High             0
Low              0
Close            0
Volume           0
Dividends        0
Stock Splits     0
Capital Gains    0
dtype: int64

Пропущенные значения после очистки:
Open             0
High             0
Low              0
Close            0
Volume           0
Dividends        0
Stock Splits     0
Capital Gains    0
dtype: int64

Этот подход к очистке данных основан на статистических методах и учитывает специфику финансовых временных рядов. В production-системах я также рекомендую логировать все изменения данных для последующего аудита и анализа качества.

Читайте также:  Матричные операции в финансах и биржевой аналитике

Обогащение данных yfinance

Сырые OHLCV данные редко используются напрямую в серьезных торговых стратегиях. Вместо этого создаются дополнительные метрики и производные индикаторы, которые лучше отражают рыночную динамику. Однако в отличие от популярных технических индикаторов, которые я считаю малоэффективными, сосредоточимся на метриках, которые действительно используются в профессиональной среде.

Категория Примеры Применение
Returns Simple, Log, Cumulative Анализ доходности
Volatility Realized, EWMA, Parkinson Оценка риска
Volume Relative, VWAP, OBV Анализ ликвидности
Momentum Price change, MA ratios Трендовый анализ
Microstructure Spread, Impact Торговые издержки

Вот как можно реализовать создание производных метрик с помощью Python.

def calculate_advanced_metrics(df):
    df = df.copy()
    
    # Логарифмические доходности
    df['log_returns'] = np.log(df['Close'] / df['Close'].shift(1))
    
    # Realized volatility (реализованная волатильность)
    df['realized_vol'] = df['log_returns'].rolling(window=20).std() * np.sqrt(252)
    
    # Внутридневной диапазон
    df['daily_range'] = (df['High'] - df['Low']) / df['Close']
    
    # True Range (более робастная мера волатильности)
    df['prev_close'] = df['Close'].shift(1)
    df['true_range'] = np.maximum(
        df['High'] - df['Low'],
        np.maximum(
            abs(df['High'] - df['prev_close']),
            abs(df['Low'] - df['prev_close'])
        )
    )
    
    # Volume-weighted метрики
    df['vwap'] = (df['Volume'] * (df['High'] + df['Low'] + df['Close']) / 3).cumsum() / df['Volume'].cumsum()
    
    # Микроструктурные метрики
    df['bid_ask_spread_proxy'] = (df['High'] - df['Low']) / df['Close']
    df['price_impact'] = abs(df['log_returns']) / (df['Volume'] / df['Volume'].rolling(20).mean())
    
    # Momentum с различными таймфреймами
    for period in [5, 10, 20, 60]:
        df[f'momentum_{period}d'] = df['Close'] / df['Close'].shift(period) - 1
    
    # Корреляция с общим рынком (если это не индекс)
    # Будет реализована при наличии данных по рынку
    
    return df.drop(['prev_close'], axis=1)

# Применение к данным
aapl = yf.Ticker("AAPL")
aapl_data = aapl.history(period="1y")
aapl_enhanced = calculate_advanced_metrics(aapl_data)

# Анализ полученных метрик
print("Корреляции между метриками:")
correlation_matrix = aapl_enhanced[['log_returns', 'realized_vol', 'daily_range', 'momentum_20d']].corr()
print(correlation_matrix)
Корреляции между метриками:
              log_returns  realized_vol  daily_range  momentum_20d
log_returns      1.000000      0.026946    -0.063236      0.162641
realized_vol     0.026946      1.000000     0.406199     -0.387658
daily_range     -0.063236      0.406199     1.000000     -0.447095
momentum_20d     0.162641     -0.387658    -0.447095      1.000000

Эти метрики гораздо более информативны для построения торговых стратегий, чем традиционные индикаторы типа скользящих средних, MACD или RSI. Realized volatility, например, лучше отражает текущие рыночные условия, чем скользящие средние, а микроструктурные метрики хорошо отражают оценку ликвидности и импакт различных факторов на текущую цену.

Тикеры и правила их формирования в yfinance

Корректное указание тикеров критически важно для получения нужных данных. Yahoo Finance использует собственную систему обозначений, которая может отличаться от других источников данных. Понимание этих правил поможет избежать ошибок при работе с международными активами и различными классами инструментов.

Основные принципы формирования тикеров:

Американские акции используют стандартные биржевые символы без суффиксов: AAPL, MSFT, GOOGL. Международные акции требуют добавления биржевого суффикса через точку: европейские акции используют местные коды бирж (.L для Лондона, .PA для Парижа), азиатские рынки имеют собственные обозначения (.T для Токио, .HK для Гонконга).

Валютные пары записываются в формате BASECURRENCY=X, где базовая валюта указывается относительно USD: EURUSD=X, GBPUSD=X. Для кросс-курсов используется формат CURRENCY1CURRENCY2=X: EURJPY=X, GBPCHF=X.

Криптовалюты обозначаются как SYMBOL-USD для пар к доллару: BTC-USD, ETH-USD, или SYMBOL1-SYMBOL2 для других пар: BTC-EUR, ETH-BTC. ETF и индексы обычно используют стандартные тикеры без модификаций: SPY, QQQ, ^GSPC (для индекса S&P 500).

Фьючерсы имеют сложную систему обозначений, включающую базовый символ, месяц поставки и год: CL=F для нефти, GC=F для золота, с указанием конкретного контракта: например, CLZ24.NYM для декабрьского контракта нефти 2024 года.

Популярные тикеры по категориям

Для быстрого анализа, обучения, тестирования гипотез важно запомнить наизусть наиболее ликвидные и популярные тикеры в каждой категории. Эти активы обычно имеют лучшее качество данных, что позволяет легко проверять свои торговые идеи, прежде чем погружаться в более нишевые активы.

ETF (Exchange-Traded Funds):

  • SPY – SPDR S&P 500 ETF (отслеживает индекс S&P 500);
  • QQQ – Invesco QQQ Trust (следует за Nasdaq-100);
  • VTI – Vanguard Total Stock Market (охватывает весь американский рынок);
  • IWM – iShares Russell 2000 (малая капитализация США);
  • EFA – iShares MSCI EAFE (развитые рынки кроме США).

Акции США (Blue Chips):

  • AAPL – Apple Inc;
  • MSFT – Microsoft Corp;
  • GOOGL – Alphabet Inc;
  • AMZN – Amazon.com Inc;
  • TSLA – Tesla Inc.

Валютные пары (Major Currencies):

  • EURUSD=X (евро к доллару США);
  • GBPUSD=X (британский фунт к доллару);
  • USDJPY=X (доллар США к японской йене);
  • USDCAD=X (доллар США к канадскому доллару);
  • AUDUSD=X (австралийский доллар к доллару США).

Криптовалюты (Top Market Cap):

  • BTC-USD – Bitcoin;
  • ETH-USD – Ethereum;
  • BNB-USD – Binance Coin;
  • ADA-USD – Cardano;
  • SOL-USD – Solana.

Фьючерсы:

  • CL=F – Crude Oil (нефть WTI, биржа NYMEX);
  • GC=F – Gold (золото, биржа COMEX);
  • ES=F – E-mini S&P 500 (фьючерс на S&P 500, биржа CME);
  • NQ=F – E-mini Nasdaq-100 (фьючерс на Nasdaq, биржа CME);
  • ZN=F – 10-Year Treasury (10-летние казначейские облигации, биржа CBOT).

Работа с множественными активами

Эффективная загрузка данных для портфеля

При работе с портфелями из десятков или сотен активов эффективность загрузки данных становится критически важной. Библиотека yfinance предоставляет метод download(), который позволяет загружать данные для множества символов одновременно, что существенно быстрее последовательных запросов.

Параметр Описание Пример
tickers Список тикеров «AAPL MSFT» или [«AAPL», «MSFT»]
period Период данных «1y», «6mo», «max»
interval Интервал «1d», «1h», «5m»
group_by Группировка данных «ticker» или «column»
auto_adjust Корректировка цен True/False
prepost Расширенные часы True/False
threads Количество потоков True (по умолчанию)
proxy Настройки прокси None

Вот как можно загрузить данные сразу по нескольким активам с помощью метода donwload():

import yfinance as yf
import pandas as pd
from concurrent.futures import ThreadPoolExecutor, as_completed
import time

# Список символов для анализа
tech_symbols = ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'META', 'TSLA', 'NVDA', 'NFLX']
financial_symbols = ['JPM', 'BAC', 'WFC', 'GS', 'MS', 'C']
all_symbols = tech_symbols + financial_symbols

def download_portfolio_data_efficient(symbols, period="1y", **kwargs):
    """
    Эффективная загрузка данных для портфеля
    """
    # Метод 1: Bulk download через yfinance
    start_time = time.time()
    bulk_data = yf.download(symbols, period=period, group_by='ticker', **kwargs)
    bulk_time = time.time() - start_time
    
    print(f"Bulk download выполнен за {bulk_time:.2f} секунд")
    
    # Преобразование в удобный формат
    portfolio_data = {}
    for symbol in symbols:
        if len(symbols) > 1:
            try:
                portfolio_data[symbol] = bulk_data[symbol].dropna()
            except KeyError:
                print(f"Данные для {symbol} не найдены")
                continue
        else:
            portfolio_data[symbol] = bulk_data.dropna()
    
    return portfolio_data, bulk_time

def download_portfolio_concurrent(symbols, period="1y", max_workers=5):
    """
    Конкурентная загрузка данных
    """
    start_time = time.time()
    portfolio_data = {}
    
    def fetch_symbol_data(symbol):
        try:
            ticker = yf.Ticker(symbol)
            data = ticker.history(period=period)
            return symbol, data
        except Exception as e:
            print(f"Ошибка при загрузке {symbol}: {e}")
            return symbol, pd.DataFrame()
    
    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        future_to_symbol = {executor.submit(fetch_symbol_data, symbol): symbol 
                           for symbol in symbols}
        
        for future in as_completed(future_to_symbol):
            symbol, data = future.result()
            if not data.empty:
                portfolio_data[symbol] = data
    
    concurrent_time = time.time() - start_time
    print(f"Concurrent download выполнен за {concurrent_time:.2f} секунд")
    
    return portfolio_data, concurrent_time

# Сравнение методов
print("Сравнение методов загрузки данных:")
bulk_data, bulk_time = download_portfolio_data_efficient(all_symbols)
concurrent_data, concurrent_time = download_portfolio_concurrent(all_symbols)

print(f"\nРезультаты:")
print(f"Bulk method: {len(bulk_data)} символов за {bulk_time:.2f}с")
print(f"Concurrent method: {len(concurrent_data)} символов за {concurrent_time:.2f}с")
Сравнение методов загрузки данных:
YF.download() has changed argument auto_adjust default to True
[*********************100%***********************]  14 of 14 completed
Bulk download выполнен за 1.91 секунд
Concurrent download выполнен за 0.54 секунд

Результаты:
Bulk method: 14 символов за 1.91с
Concurrent method: 14 символов за 0.54с

В моей практике bulk download через yf.download() обычно оказывается быстрее для больших портфелей, но concurrent подход дает больше контроля над обработкой ошибок и позволяет продолжать работу даже при сбоях в загрузке отдельных символов.

Синхронизация временных рядов и обработка разных торговых расписаний

Одной из серьезных проблем при работе с международными портфелями является различие в торговых расписаниях разных рынков. Американские, европейские и азиатские биржи работают в разное время, имеют разные праздники и периоды технического обслуживания. Это создает сложности при построении корреляционных матриц и риск-моделей.

from datetime import datetime, timedelta
import pandas as pd
import numpy as np

def create_synchronized_portfolio(symbols_by_market, period="2y"):
    """
    Создание синхронизированного портфеля с учетом торговых расписаний
    """
    # Определение символов по рынкам
    us_symbols = symbols_by_market.get('US', [])
    europe_symbols = symbols_by_market.get('EU', [])
    asia_symbols = symbols_by_market.get('ASIA', [])
    
    all_data = {}
    
    # Загрузка данных для каждого рынка
    for market, symbols in symbols_by_market.items():
        if symbols:
            market_data = yf.download(symbols, period=period, group_by='ticker')
            
            for symbol in symbols:
                if len(symbols) > 1:
                    try:
                        all_data[f"{symbol}_{market}"] = market_data[symbol]['Close']
                    except KeyError:
                        continue
                else:
                    all_data[f"{symbol}_{market}"] = market_data['Close']
    
    # Создание общего DataFrame
    portfolio_df = pd.DataFrame(all_data)
    
    # Синхронизация данных
    # Метод 1: Forward fill для пропущенных значений
    portfolio_sync = portfolio_df.fillna(method='ffill')
    
    # Метод 2: Интерполяция только для коротких пропусков (до 3 дней)
    portfolio_interp = portfolio_df.copy()
    for col in portfolio_interp.columns:
        # Заполнение пропусков только если они не превышают 3 дня
        mask = portfolio_interp[col].isna()
        groups = (mask != mask.shift()).cumsum()
        
        for group_id in groups[mask].unique():
            group_mask = (groups == group_id) & mask
            if group_mask.sum() <= 3:  # Не более 3 дней пропуска
                portfolio_interp.loc[group_mask, col] = portfolio_interp[col].interpolate().loc[group_mask]
    
    # Расчет доходностей с учетом временных зон
    returns_sync = portfolio_sync.pct_change().dropna()
    returns_interp = portfolio_interp.pct_change().dropna()
    
    # Анализ качества синхронизации
    print("Анализ синхронизации данных:")
    print(f"Общий период: {portfolio_df.index.min()} - {portfolio_df.index.max()}")
    print(f"Количество торговых дней: {len(portfolio_df)}")
    
    missing_data = portfolio_df.isna().sum()
    print("\nПропуски данных по активам:")
    for asset, missing_count in missing_data.items():
        missing_pct = (missing_count / len(portfolio_df)) * 100
        print(f"{asset}: {missing_count} дней ({missing_pct:.1f}%)")
    
    return {
        'raw_data': portfolio_df,
        'forward_fill': portfolio_sync,
        'interpolated': portfolio_interp,
        'returns_sync': returns_sync,
        'returns_interp': returns_interp
    }

# Пример использования
portfolio_symbols = {
    'US': ['AAPL', 'MSFT', 'GOOGL'],
    'EU': ['ASML', 'SAP', 'NESN.SW'],
    'ASIA': ['TSM', '7203.T', '005930.KS']  # TSMC, Toyota, Samsung
}

synchronized_portfolio = create_synchronized_portfolio(portfolio_symbols)

# Анализ корреляций между рынками
correlation_matrix = synchronized_portfolio['returns_sync'].corr()
print("\nКорреляции доходностей между активами:")
print(correlation_matrix.round(3))
Анализ синхронизации данных:
Общий период: 2023-06-12 00:00:00 - 2025-06-12 00:00:00
Количество торговых дней: 522

Пропуски данных по активам:
AAPL_US: 20 дней (3.8%)
MSFT_US: 20 дней (3.8%)
GOOGL_US: 20 дней (3.8%)
ASML_EU: 20 дней (3.8%)
SAP_EU: 20 дней (3.8%)
NESN.SW_EU: 20 дней (3.8%)
TSM_ASIA: 20 дней (3.8%)
7203.T_ASIA: 31 дней (5.9%)
005930.KS_ASIA: 35 дней (6.7%)

Корреляции доходностей между активами:
                AAPL_US  MSFT_US  GOOGL_US  ASML_EU  SAP_EU  NESN.SW_EU  TSM_ASIA  7203.T_ASIA  005930.KS_ASIA
AAPL_US           1.000    0.543     0.466    0.412   0.426       0.013     0.390        0.054           0.107
MSFT_US           0.543    1.000     0.536    0.485   0.510      -0.099     0.502        0.062           0.080
GOOGL_US          0.466    0.536     1.000    0.379   0.382      -0.076     0.404        0.037           0.084
ASML_EU           0.412    0.485     0.379    1.000   0.559      -0.053     0.707        0.022           0.048
SAP_EU            0.426    0.510     0.382    0.559   1.000       0.021     0.509        0.138           0.093
NESN.SW_EU        0.013   -0.099    -0.076   -0.053   0.021       1.000    -0.101        0.065           0.108
TSM_ASIA          0.390    0.502     0.404    0.707   0.509      -0.101     1.000        0.061           0.120
7203.T_ASIA       0.054    0.062     0.037    0.022   0.138       0.065     0.061        1.000           0.329
005930.KS_ASIA    0.107    0.080     0.084    0.048   0.093       0.108     0.120        0.329           1.000

Правильная синхронизация данных особенно важна для риск-менеджмента и портфельной оптимизации. Неправильная обработка пропусков может привести к искажению корреляций и недооценке рисков.

Читайте также:  23 способа визуализации котировок с помощью Mplfinance

Продвинутые техники работы с API

Оптимизация запросов и управление нагрузкой

Yahoo Finance, как и любой публичный API, имеет неофициальные ограничения на частоту запросов. Хотя жестких rate limits нет, слишком агрессивные запросы могут привести к временной блокировке IP-адреса. Вот почему если вы планируете использовать yfinance в production-системах критически важно реализовать метод intelligent rate limiting.

import time
import requests
from datetime import datetime, timedelta
import threading
from collections import defaultdict, deque

class YFinanceRateLimiter:
    """
    Умный rate limiter для yfinance с адаптивным поведением
    """
    
    def __init__(self, requests_per_minute=60, burst_size=10):
        self.requests_per_minute = requests_per_minute
        self.burst_size = burst_size
        self.request_times = deque()
        self.lock = threading.Lock()
        self.consecutive_errors = 0
        self.base_delay = 1.0  # Базовая задержка в секундах
        
    def wait_if_needed(self):
        """Ожидание перед следующим запросом при необходимости"""
        with self.lock:
            now = datetime.now()
            
            # Удаляем старые запросы (старше минуты)
            while self.request_times and (now - self.request_times[0]).total_seconds() > 60:
                self.request_times.popleft()
            
            # Проверяем лимиты
            if len(self.request_times) >= self.requests_per_minute:
                sleep_time = 60 - (now - self.request_times[0]).total_seconds()
                if sleep_time > 0:
                    time.sleep(sleep_time)
            
            # Адаптивная задержка при ошибках
            if self.consecutive_errors > 0:
                adaptive_delay = self.base_delay * (2 ** min(self.consecutive_errors, 5))
                time.sleep(adaptive_delay)
            
            self.request_times.append(now)
    
    def record_success(self):
        """Запись успешного запроса"""
        self.consecutive_errors = 0
    
    def record_error(self):
        """Запись ошибки запроса"""
        self.consecutive_errors += 1

def robust_ticker_fetch(symbol, rate_limiter, **kwargs):
    """
    Robust загрузка данных с rate limiting и retry логикой
    """
    max_retries = 3
    
    for attempt in range(max_retries):
        try:
            rate_limiter.wait_if_needed()
            
            ticker = yf.Ticker(symbol)
            data = ticker.history(**kwargs)
            
            if data.empty:
                raise ValueError(f"Пустые данные для {symbol}")
            
            rate_limiter.record_success()
            return data
            
        except Exception as e:
            rate_limiter.record_error()
            
            if attempt == max_retries - 1:
                print(f"Не удалось загрузить данные для {symbol} после {max_retries} попыток: {e}")
                return pd.DataFrame()
            
            # Экспоненциальная задержка при повторных попытках
            time.sleep(2 ** attempt)
    
    return pd.DataFrame()

# Пример использования продвинутого rate limiting
rate_limiter = YFinanceRateLimiter(requests_per_minute=45, burst_size=5)

symbols = ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'TSLA', 'META', 'NVDA', 'NFLX', 'ORCL', 'CRM']
portfolio_data = {}

print("Загрузка данных с intelligent rate limiting:")
start_time = time.time()

for symbol in symbols:
    data = robust_ticker_fetch(symbol, rate_limiter, period="6mo")
    if not data.empty:
        portfolio_data[symbol] = data
        print(f"✓ {symbol}: {len(data)} записей")
    else:
        print(f"✗ {symbol}: ошибка загрузки")

total_time = time.time() - start_time
print(f"\nВсего загружено: {len(portfolio_data)} активов за {total_time:.2f} секунд")
Загрузка данных с intelligent rate limiting:
✓ AAPL: 123 записей
✓ MSFT: 123 записей
✓ GOOGL: 123 записей
✓ AMZN: 123 записей
✓ TSLA: 123 записей
✓ META: 123 записей
✓ NVDA: 123 записей
✓ NFLX: 123 записей
✓ ORCL: 123 записей
✓ CRM: 123 записей

Всего загружено: 10 активов за 1.67 секунд

Этот подход позволяет адаптироваться к изменяющимся условиям API и минимизировать риск блокировки при интенсивном использовании.

Кэширование данных и offline режим

В профессиональных системах критически важно обеспечить возможность работы при недоступности внешних API. Реализация intelligent caching позволяет не только ускорить повторные запросы, но и обеспечить постоянство бизнес-процессов даже при наличии сбоев в сети.

import os
import pickle
import hashlib
from pathlib import Path
import pandas as pd
from datetime import datetime, timedelta

class YFinanceCache:
    """
    Интеллектуальная система кэширования для yfinance
    """
    
    def __init__(self, cache_dir="./yfinance_cache", default_ttl=3600):
        self.cache_dir = Path(cache_dir)
        self.cache_dir.mkdir(exist_ok=True)
        self.default_ttl = default_ttl  # TTL в секундах
        
    def _get_cache_key(self, symbol, start, end, interval, **kwargs):
        """Генерация уникального ключа кэша"""
        key_data = f"{symbol}_{start}_{end}_{interval}_{sorted(kwargs.items())}"
        return hashlib.md5(key_data.encode()).hexdigest()
    
    def _get_cache_path(self, cache_key):
        """Путь к файлу кэша"""
        return self.cache_dir / f"{cache_key}.pkl"
    
    def _is_cache_valid(self, cache_path, ttl=None):
        """Проверка валидности кэша"""
        if not cache_path.exists():
            return False
        
        ttl = ttl or self.default_ttl
        cache_age = time.time() - cache_path.stat().st_mtime
        return cache_age < ttl
    
    def get_cached_data(self, symbol, start, end, interval='1d', **kwargs):
        """Получение данных из кэша"""
        cache_key = self._get_cache_key(symbol, start, end, interval, **kwargs)
        cache_path = self._get_cache_path(cache_key)
        
        # Для intraday данных используем короткий TTL
        ttl = 300 if interval in ['1m', '2m', '5m', '15m', '30m', '60m', '90m'] else self.default_ttl
        
        if self._is_cache_valid(cache_path, ttl):
            try:
                with open(cache_path, 'rb') as f:
                    cached_data = pickle.load(f)
                print(f"Данные для {symbol} загружены из кэша")
                return cached_data
            except Exception as e:
                print(f"Ошибка чтения кэша для {symbol}: {e}")
        
        return None
    
    def cache_data(self, data, symbol, start, end, interval='1d', **kwargs):
        """Сохранение данных в кэш"""
        cache_key = self._get_cache_key(symbol, start, end, interval, **kwargs)
        cache_path = self._get_cache_path(cache_key)
        
        try:
            with open(cache_path, 'wb') as f:
                pickle.dump(data, f)
        except Exception as e:
            print(f"Ошибка сохранения кэша для {symbol}: {e}")
    
    def clear_cache(self, older_than_days=7):
        """Очистка старого кэша"""
        cutoff_time = time.time() - (older_than_days * 24 * 3600)
        removed_count = 0
        
        for cache_file in self.cache_dir.glob("*.pkl"):
            if cache_file.stat().st_mtime < cutoff_time:
                cache_file.unlink()
                removed_count += 1
        
        print(f"Удалено {removed_count} старых файлов кэша")

def cached_ticker_history(symbol, cache_manager, **kwargs):
    """
    Получение исторических данных с кэшированием
    """
    start = kwargs.get('start')
    end = kwargs.get('end')
    period = kwargs.get('period', '1y')
    interval = kwargs.get('interval', '1d')
    
    # Нормализация параметров времени
    if period and not start:
        end_date = datetime.now().date()
        if period == '1d':
            start_date = end_date - timedelta(days=1)
        elif period == '5d':
            start_date = end_date - timedelta(days=5)
        elif period == '1mo':
            start_date = end_date - timedelta(days=30)
        elif period == '3mo':
            start_date = end_date - timedelta(days=90)
        elif period == '6mo':
            start_date = end_date - timedelta(days=180)
        elif period == '1y':
            start_date = end_date - timedelta(days=365)
        elif period == '2y':
            start_date = end_date - timedelta(days=730)
        elif period == '5y':
            start_date = end_date - timedelta(days=1825)
        else:
            start_date = end_date - timedelta(days=365)
        
        start = start_date
        end = end_date
    
    # Попытка получить данные из кэша
    cached_data = cache_manager.get_cached_data(symbol, start, end, interval, **kwargs)
    if cached_data is not None:
        return cached_data
    
    # Загрузка данных из API
    try:
        ticker = yf.Ticker(symbol)
        data = ticker.history(**kwargs)
        
        if not data.empty:
            # Сохранение в кэш
            cache_manager.cache_data(data, symbol, start, end, interval, **kwargs)
            print(f"Данные для {symbol} загружены из API и сохранены в кэш")
        
        return data
        
    except Exception as e:
        print(f"Ошибка загрузки данных для {symbol}: {e}")
        return pd.DataFrame()

# Использование кэширования
cache_manager = YFinanceCache(cache_dir="./financial_cache", default_ttl=1800)

# Первый запрос - загрузка из API
apple_data = cached_ticker_history('AAPL', cache_manager, period='1y')
print(f"Загружено {len(apple_data)} записей для AAPL")

# Второй запрос - загрузка из кэша
apple_data_cached = cached_ticker_history('AAPL', cache_manager, period='1y')
print(f"Повторно загружено {len(apple_data_cached)} записей для AAPL")

# Очистка старого кэша
cache_manager.clear_cache(older_than_days=3)
Данные для AAPL загружены из API и сохранены в кэш
Загружено 250 записей для AAPL
Данные для AAPL загружены из кэша
Повторно загружено 250 записей для AAPL
Удалено 0 старых файлов кэша

Система кэширования особенно важна для фреймворков бэктестинга, где одни и те же данные могут запрашиваться многократно при оптимизации параметров стратегии.

Заключение

В этой статье мы рассмотрели все ключевые аспекты работы с финансовыми данными через API Yahoo Finance.

Эта библиотека для Python представляет собой мощный инструмент для получения качественных рыночных данных без необходимости дорогостоящих подписок на коммерческие источники. yfinance обеспечивает доступ к широкому спектру финансовых инструментов — от американских акций до международных ETF, криптовалют и товарных фьючерсов. Архитектура библиотеки, построенная вокруг класса Ticker и функции download(), предоставляет гибкие возможности для получения как исторических данных, так и фундаментальной информации о компаниях.

С точки зрения практики, я рекомендую использовать yfinance как основной источник данных для исследовательских задач, бэктестинга торговых стратегий и пет-проектов. Библиотека особенно хорошо подходит для работы с американскими активами, где качество и полнота данных максимальны. При работе с международными рынками следует учитывать возможные особенности в форматах тикеров и временных зонах.

В контексте современного quantitative finance библиотека yfinance конечно слабовата, и скорее служит отличной отправной точкой, поскольку позволяет получить быстрый доступ к рыночным данным без сложных процедур аутентификации и высоких затрат на подписки. Однако для серьезной production-работы, по мере роста требований к качеству данных, скорости обновления и глубине исторических данных стоит рассматривать переход на профессиональные решения вроде Bloomberg API, Refinitiv Eikon или специализированных провайдеров вроде Alpha Vantage и Quandl.