Блог им. vtvladim

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

  Сегодня обсуждалась тема о том, что использование ряда зарубежных сайтов  для скачивания исторических данных стало недоступно. Добрый человек подсказал решение проблемы на питоне с использованием yfinance. При помощи DeepSeak создал программу для выгрузки данных. Возможно кому-то еще будет полезно.
   Для использования программы надо создать в текущей папке с кодом две папки: InData и OutData. В первой папке создайте текстовый файл со списком тикеров (активов), разделенных точкой с запятой (;) с расширением txt. Во второй папке будет сохранен файл «текущая дата.txt» с данными разделителем точка с запятой. Данные по всем заданным тикерам записываются в один файл последовательно. Формат вывода:
                              Тикер / Data  Open  High  Low  Close Volume /  данные по дням за период
   После запуска — выбрать период дат, выбрать файл с исходным списком активов, запустить скачивание данных.
     Сам код следующий:

import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import yfinance as yf
import pandas as pd
import numpy as np
import os
from datetime import datetime

class YFinanceDownloader:
def __init__(self, root):
self.root = root
self.root.title(«YFinance Data Downloader»)
self.root.geometry(«600x400»)

self.create_widgets()

def create_widgets(self):
# Заголовок
title_label = ttk.Label(self.root, text=«Выгрузка данных торгов», font=(«Arial», 14, «bold»))
title_label.pack(pady=10)

# Фрейм для ввода дат
date_frame = ttk.Frame(self.root)
date_frame.pack(pady=10, padx=20, fill=«x»)

ttk.Label(date_frame, text=«Период дат (ДД.ММ.ГГГГ — ДД.ММ.ГГГГ):»).pack(anchor=«w»)

self.date_entry = ttk.Entry(date_frame, width=30)
self.date_entry.pack(pady=5, fill=«x»)
self.date_entry.insert(0, «01.10.2025 — 31.10.2025»)

# Фрейм для выбора файла
file_frame = ttk.Frame(self.root)
file_frame.pack(pady=10, padx=20, fill=«x»)

ttk.Label(file_frame, text=«Файл с тикерами:»).pack(anchor=«w»)

file_select_frame = ttk.Frame(file_frame)
file_select_frame.pack(pady=5, fill=«x»)

self.file_path = tk.StringVar()
self.file_entry = ttk.Entry(file_select_frame, textvariable=self.file_path, width=40)
self.file_entry.pack(side=«left», fill=«x», expand=True)

ttk.Button(file_select_frame, text=«Обзор», command=self.browse_file).pack(side=«left», padx=5)

# Кнопка выполнения
ttk.Button(self.root, text=«Выгрузить данные», command=self.download_data).pack(pady=20)

# Прогресс бар
self.progress = ttk.Progressbar(self.root, mode='indeterminate')
self.progress.pack(pady=10, padx=20, fill=«x»)

# Текстовое поле для логов
self.log_text = tk.Text(self.root, height=10, width=70)
self.log_text.pack(pady=10, padx=20, fill=«both», expand=True)

# Добавляем скроллбар для текстового поля
scrollbar = ttk.Scrollbar(self.log_text)
scrollbar.pack(side=«right», fill=«y»)
self.log_text.config(yscrollcommand=scrollbar.set)
scrollbar.config(command=self.log_text.yview)

def browse_file(self):
filename = filedialog.askopenfilename(
initialdir=«InData»,
title=«Выберите файл с тикерами»,
filetypes=((«Text files», "*.txt"), («All files», "*.*"))
)
if filename:
self.file_path.set(filename)

def log_message(self, message):
self.log_text.insert(«end», f"{datetime.now().strftime('%H:%M:%S')} — {message}\n")
self.log_text.see(«end»)
self.root.update()

def parse_dates(self, date_range):
""«Парсинг дат из формата ДД.ММ.ГГГГ»""
try:
start_str, end_str = date_range.split("-")
start_date = datetime.strptime(start_str.strip(), "%d.%m.%Y").strftime("%Y-%m-%d")
end_date = datetime.strptime(end_str.strip(), "%d.%m.%Y").strftime("%Y-%m-%d")
return start_date, end_date
except ValueError as e:
raise ValueError(«Неверный формат даты. Используйте: ДД.ММ.ГГГГ — ДД.ММ.ГГГГ»)

def read_tickers(self, filepath):
""«Чтение тикеров из файла»""
try:
with open(filepath, 'r', encoding='utf-8') as file:
content = file.read().strip()
tickers = [ticker.strip() for ticker in content.split(';') if ticker.strip()]
return tickers
except Exception as e:
raise Exception(f«Ошибка чтения файла: {e}»)

def download_data(self):
try:
self.progress.start()
self.log_text.delete(1.0, «end»)

# Проверка введенных данных
if not self.file_path.get():
messagebox.showerror(«Ошибка», «Выберите файл с тикерами»)
return

if not self.date_entry.get():
messagebox.showerror(«Ошибка», «Введите период дат»)
return

# Парсинг дат
self.log_message(«Парсинг дат...»)
start_date, end_date = self.parse_dates(self.date_entry.get())
self.log_message(f«Период: {start_date} — {end_date}»)

# Чтение тикеров
self.log_message(«Чтение тикеров из файла...»)
tickers = self.read_tickers(self.file_path.get())
self.log_message(f«Найдено тикеров: {len(tickers)}»)

if not tickers:
messagebox.showerror(«Ошибка», «Файл не содержит тикеров»)
return

# Создание папки OutData если её нет
if not os.path.exists(«OutData»):
os.makedirs(«OutData»)
self.log_message(«Создана папка OutData»)

# Создание имени файла с текущей датой
current_date = datetime.now().strftime("%d-%m-%Y")
output_filename = f«OutData/{current_date}.txt»

# Открываем файл для записи
with open(output_filename, 'w', encoding='utf-8') as f:
total_tickers_saved = 0

for ticker in tickers:
try:
self.log_message(f«Обработка тикера {ticker}...»)

# Загружаем данные для текущего тикера
data = yf.download(ticker, start=start_date, end=end_date, progress=False)

if data.empty:
self.log_message(f«Предупреждение: нет данных для {ticker}»)
continue

self.log_message(f«Загружено {len(data)} строк для {ticker}»)

# Записываем заголовок тикера
f.write(f«Тикер {ticker}\n»)

# Записываем названия колонок
f.write(«Date;Open;High;Low;Close;Volume\n»)

# Подготовка и запись данных
rows_written = 0
for index, row in data.iterrows():
try:
# Преобразуем дату в формат ДД.ММ.ГГГГ
date_str = index.strftime('%d.%m.%Y')

# Получаем значения как числа
open_price = float(row['Open'])
high_price = float(row['High'])
low_price = float(row['Low'])
close_price = float(row['Close'])
volume = int(row['Volume'])

# Форматируем строку данных
data_line = f"{date_str};{open_price:.2f};{high_price:.2f};{low_price:.2f};{close_price:.2f};{volume}\n"
f.write(data_line)
rows_written += 1

except Exception as row_error:
continue

# Добавляем пустую строку между тикерами (кроме последнего)
if ticker != tickers[-1]:
f.write("\n")

total_tickers_saved += 1
self.log_message(f«Записано {rows_written} строк для {ticker}»)

except Exception as e:
self.log_message(f«Ошибка при обработке {ticker}: {str(e)}»)

self.progress.stop()

# Проверяем содержимое файла
if os.path.exists(output_filename):
with open(output_filename, 'r', encoding='utf-8') as check_file:
content = check_file.read()
file_size = len(content)

self.log_message(f«Файл создан: {output_filename}»)
self.log_message(f«Размер файла: {file_size} байт»)

if file_size > 0:
messagebox.showinfo(«Успех», f«Данные успешно выгружены для {total_tickers_saved} из {len(tickers)} тикеров\nФайл: {output_filename}»)
else:
messagebox.showwarning(«Предупреждение», f«Файл создан, но пустой: {output_filename}»)
else:
messagebox.showerror(«Ошибка», f«Файл не был создан: {output_filename}»)

except Exception as e:
self.progress.stop()
self.log_message(f«Ошибка: {str(e)}»)
messagebox.showerror(«Ошибка», str(e))

def main():
# Создаем папки если их нет
if not os.path.exists(«InData»):
os.makedirs(«InData»)
print(«Создана папка InData — поместите туда файлы с тикерами»)

if not os.path.exists(«OutData»):
os.makedirs(«OutData»)
print(«Создана папка OutData — туда будут сохраняться результаты»)

root = tk.Tk()
app = YFinanceDownloader(root)
root.mainloop()

if __name__ == "__main__":
main()



756 | ★1
16 комментариев
Меня зациклило на одной задаче, не могу подобрать длину окна истории, которое требуется для обучения модели. Перепробовал много вариантов, но ни один не работает точно. Ищу как эту задачу решают другие, но по теме почти пусто, даже в книгах… Вот думаю, почему бы не спросить у более подкованных в математике и физике, например у тебя :) Наверняка же приходилось сталкиваться с похожей задачей, как считаешь, есть ли хорошее аналитическое решение у сей проблемы, может какую-то теорию сюда можно притянуть? 
avatar
Ладно стебаться ))))  Был когда то неплохо подкованный, а теперь довольствуюсь тем, что осталось )))
Конкретизируй вопрос: хочешь получить формализованную оценку достаточной глубины ретроспективы для обучения модели? Верно понял вопрос? Если верно, то ответ зависит от «метода обучения» или способа расчетов.  

В правильно заданом вопросе есть уже половина ответа и это более-менее понятно:) Дело тут даже не в оценке, ведь чтобы ее получить — надо:

a) сперва что-то с чем-то сравнить. И это, на мой взгляд,  больше похоже на эмпирический метод.

b) потратить время на перебор вариантов. 

Это всё допустимо, но наверное можно попытаться вычислить шаг до получения этой оценки — как-то аналитически и «заранее» посчитать длину этой ретроспективы опираясь на характеристики самих данных 

 

avatar
Serg_Mich, Понимаю, что не хочешь «палить поляну». Но на такую туманную постановку вопроса невозможно четко ответить. 
Пофантазирую: к примеру, ты ищешь некий паттерн некой закономерности. Задал глубину ретроспективы N, не нашел, увеличиваешь глубину, пока не нашел на глубине N1. Хватит? Нет, поскольку следует найти более одного подтверждения работы паттерна. Сколько — зависит от твоей ТС и взгляда. В общем случае лучше не ограничиваться одним паттерном, чтобы избежать подгонки, а иметь несколько. Для расчетного метода идея другая. 
Ответ на вскидку, не особо кубатуря в проблеме. Да и проблема туманна.... 

Можно и паттернами мерять, можно твоими величинами, думаю принцип должен быть похожим. Если паттернами, тогда, что если на некой глубине ретроспективы встречается N-повторений одного паттерна, допустим 20 или 120 и статистика вроде как набрана. Вот тут, чтобы не получить переобучение или наоборот шум на выходе, как понять, а в идеале вычислить заранее и без перебора, этих N уже хватит или еще недостаточно? (если отталкиваться от того, что глубина задается N-повторениями одного паттерна, но мне эта идея не очень). Но в целом твоя идея понятна, она +- похожа на мою. Ладно, буду думать, это единственно нерешенная задача осталась в этой чертовщине :)

avatar
Когда ты набрал достаточную статистику паттерна (определим это так: сочетание параметров -> движение цены), на этой же ретроспективе следует иметь статистику иное сочетание параметров -> такое же движение цены. А для такой идентификации надо иметь способ ранжирования движений цены и набора сочетаний параметров. Дальше уже должно быть понятно — обучение и паттерн рабочие, если искомое движение цены подавляющим преимуществом описывается паттерном, а иные сочетания параметров сопровождаются таким же движением цены существенно реже. А в случае расчетного метода все намного проще — смотришь на качество и стабильность результата при увеличении ретроспективы, останавливаешься на оптимальном.     

«следует иметь статистику иное сочетание параметров -> такое же движение цены» — а это уже другой паттерн и задача свелась к ранжированию разных паттернов на одной выборке. Если допустим ты предполагаешь что к некому паттерну можно добавить скрытый параметр, вроде показания объема или индикатора, то по сути получаем новый патерн, с теми же проблемами статистики недобора или перебора и вопрос с длиной ретроспективы так же остатся открытым.

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

avatar
Serg_Mich, Ты неверно понял трактовку понятия «иное сочетание параметров -> такое же движение цены». Это не новый паттерн, а движение цены, аналогичное искомому движению, НО НЕ СОПРОВОЖДАЮЩЕЕСЯ СОЧЕТАНИЕМ ПАРАМЕТРОВ ТВОЕГО ПАТТЕРНА. Ты делаешь выборку «искомое движение цены» = «сочетание параметров паттерна» + «другие сочетания параметров». По барабану какие другие сочетания — главное, что они ДРУГИЕ. И вот если статистика для выборки «искомое движение цены» = «сочетание параметров паттерна» значимее статистики для «других сочетаний», тогда можешь сказать, что паттерн работает. 
Что касается моего подхода — у меня нет понятия «динамика паттерна», впрочем и паттерна как такового нет. Абсолютно численные решения с четкой формализацией количественных оценок «сочетания параметров». Поэтому я не заморачивался подобной проблемой. Для меня вопрос был найти глубину ретроспективы, на которой сохраняется относительная стабильность результата и его масштабируемость по активам. Я это решил давно. В плане динамики есть проблема с существенно сильными трендами и ярко выраженными трендовыми активами. Это проблема моего подхода в целом, поскольку указанные выше случаи имеют сильную нелинейность, и там просто иное численное решение надо применять. Но как не смешно, основная проблема не в изменении метода, а в определении, что такой момент наступил. Это аналогично вопросу определения тренда. Можно сделать только в определенном пост-факте.  
Serg_Mich, Я не говорил про дополнительные параметры, не понял с чего ты это написал. Хорошо, упрощу мысль: ты хочешь выделить из твоих параметров, которыми ты собираешься описывать и прогнозировать движение, определенное их сочетание (значение, знак, изменение и т.п. по вкусу...), которое связано с определенным движением цены. Тогда все случаи искомого тобою движения цены можно разбить на две группы: произошедшие после сочетания параметров паттерна (собственно — это то, что ты ищешь и хочешь подтвердить) и произошедшие при любом ином сочетании параметров, не совпадающим с «твоим». Собственно все — ты разделил выборки для статистических выводов, далее — по тексту. 
Нюанс тут в том, что для такой процедуры ты должен уметь четко классифицировать не только «свой набор параметров», но и в определенном смысле искомое движение цены. Ну просто вверх/вниз — это слишком общее понятие — это не монета где 2 исхода, просто вверх по большому счету — это набор вариантов вверх. Либо вводишь меру — насколько «вверх». Да даже что такое вверх/вниз — это нетривиальный вопрос в рамках классификации движения. Вообще я тебе про этот момент раньше намекал как мог уже… )))     
Дело в том, что когда выборка разбивается на паттерны то не остается никакого «любого иного сочетания параметров» которое бы не сожержалось в паттернах на данной выборке. Предположим что разнообразие паттернов покрывает всю выборку, больше нет места для маневров. И понятное дело что у движения есть градиенты силы и тд, пример упрощенный был, для демонстрации. Или ты иммеешь ввиду разбивку на тренировочные и тестовые данные? Но и тут все равно вопрос об оптимальной глубине тренировочных данных никуда не делся.
avatar
Serg_Mich, «когда выборка разбивается на паттерны то не остается никакого «любого иного сочетания параметров» — конечно не верно. Не буду утверждать абсолютную свою правоту, но ситуацию вижу проще: задача состоит в нахождении упрощенно говоря влиянии некоторого сочетания параметров на движение цены. А не в полной классификации возможных сочетаний. Далее — как формировать выборку. Наверное правильнее от результата, а это движение цены. Но можно и от „сочетаний“, сохраняя принцип идентификации (классификации) как движения, так и „сочетания“.
Вот тупой прямолинейный пример, пришедший на вскидку: сочетание Close>Open, ожидаем CloseNext>Close — это и критерий движения (твое упрощенное вверх/вниз). Что и как делаем: разбиваем ретроспективу на 2 выборки: CloseNext>Close и CloseNext<Close. Если сочетание подтверждается в первой выборке и не опровергается во второй — можно считать, что оно работает. И не требуется тут полной „оцифровки“ иных сочетаний (например, High=Open ...). Со стороны „сочетаний“ ограничиваемся классификацией „наше сочетание“/»не наше сочетание". 
Тренировку и тесты ввиду совсем не имел.
Предыдущее сообщение удалил, чтобы не «палить поляну» раньше времени, там была зацепка:) В общем получилось. Пока не самым оптимальным способом и не «на лету», зато точно. Метод «плато» называется. Я им и раньше пользовался, но в других оценках. А тут подумал, почему бы не применить и для оценки ретроспективы...
Метод заключается в анализе сходимости функции ошибки при вариации длины ретроспективы. Проводятся итерационные замеры на скользящих окнах с возрастающим шагом. Оптимальным значением n_bars считается начало устойчивого плато, где производная функции ошибки по длине окна стремится к нулю. Это точка, после которой включение дополнительных исторических данных уже не приносит новой информации (информационное насыщение), а лишь увеличивает вычислительную нагрузку или размывает актуальные закономерности.
avatar
Serg_Mich, Термин «Метод плато» мне мало о чем говорит, лукавить не буду. Из твоего краткого пояснения суть твоего подхода понял. На вскидку, что пришло на ум сразу: при таком определении оптимальной глубины ретроспективы есть риск получить подгонку модели под текущий-прошедший характер движения цены. Т.е. может произойти подмена цели — не оптимизировать модель, а оптимизировать глубину ретроспективы, на которой положения модели работают наилучшим способом. Но жестко утверждать не буду. Я схожий алгоритм использую в ТС, но с другой целью и у меня глубина задается как исходный параметр, а результатом является некое изменение параметров модели. Эта процедура делается регулярно через определенное число интервалов (тоже исходный параметр). 

В твоей модели глубина n_bars, вероятно, — константа, подобранная как некая универсальная величина. Это популярный метод, и он подходит большинству стратегий, но универсальность удобна до поры до времени. Ты сам подтверждаешь, что модель не умеет отрабатывать резкую смену волатильности — и это прямое следствие неоптимальной ретроспективы. Из-за фиксированного окна модели банально не хватает данных для адаптации к аномалиям и попытка компенсировать это системой из трех окон выглядит скорее как тактический обход проблемы, чем ее фундаментальное решение.

«при таком определении оптимальной глубины ретроспективы есть риск получить подгонку модели под текущий-прошедший характер движения цены» 

Ну, подгонкой это назвать можно, но только в самом широком смысле.
Это ближе к структурной калибровке, адаптации, а не к оптимизации под доходность. Не ищется n_back, который максимизирует прибыль или точность на тестовой выборке. Ищется минимальное окно, при котором модель стабильно идентифицирует свою же внутреннюю структуру. Это ближе к вопросу «сколько данных нужно модели, чтобы работать корректно», чем к «на каких данных модель давала лучшие сигналы».
Пока ничего лучшего не нашел, приходится «мутить Франкенштейна»

avatar

Читайте на SMART-LAB:
Фото
Три идеи с фьючерсами: US Treasuries, Саудовская Аравия, Alibaba
Алексей Девятов Предлагаем три инвестиционные идеи, которые можно реализовать с помощью фьючерсов на МосБирже: ставка на подъём цен US...
Фото
Новые размещения на рынке ВДО: облигации с купоном до 25,5%
Рассмотрим параметры двух новых размещений на рынке ВДО: облигации с фиксированным купоном от МФК «Быстроденьги» и ООО «Реиннольц». Оба...
Фото
BITCOIN: Взлёт разрешён, но сначала — обязательная проверка связи с землёй
Биткоин успешно протестировал уровень 75500 и теперь пытается закрепиться выше горизонтали 80536. Однако полноценного продолжения ралли прямо...
Фото
X5 МСФО 1 кв. 2026 г. - каким может быть ближайший дивиденд?
Компания X5 опубликовала финансовые результаты за 1 кв. 2026 года. Выручка выросла на 11,3% до 1,19 трлн руб. Валовая прибыль выросла на...

теги блога Владимиров Владимир

....все тэги



UPDONW
Новый дизайн