За годы работы с финансовыми данными я перепробовал десятки различных биржевых API. Однако когда речь заходит о быстром доступе к качественным историческим данным без лишних заморочек и долгих настроек, yfinance остается одним из самых надежных инструментов в арсенале квант-аналитика.
Эта библиотека, построенная на основе API Yahoo Finance, предоставляет программный доступ к огромному массиву финансовой информации, который многие профессионалы индустрии используют для бэктестинга стратегий и исследовательских задач. В этой статье мы рассмотрим нюансы использования yfinance, базовые методы загрузки данных, а также продвинутые техники работы с API, оптимизацию запросов и обработку особых случаев, с которыми сталкиваются профессиональные трейдеры и исследователи.
Архитектура и принципы работы yfinance
Начнем с главного: yfinance не является официальным продуктом Yahoo. Данная библиотека представляет собой Python-обертку, эксплуатирующую инфраструктуру Yahoo Finance: данные ее сайта, публичных страниц и эндпоинтов.
В отличие от официальных API, которые требуют регистрации и часто ограничивают количество запросов, yfinance работает через HTTP-запросы к тем же эндпоинтам, которые использует веб-интерфейс Yahoo Finance. Это дает нам несколько важных преимуществ:
- отсутствие rate limits в традиционном понимании;
- бесплатный доступ;
- относительно высокую скорость получения данных.
Внутренняя архитектура 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()
Рис. 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
Правильная синхронизация данных особенно важна для риск-менеджмента и портфельной оптимизации. Неправильная обработка пропусков может привести к искажению корреляций и недооценке рисков.
Продвинутые техники работы с 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.