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()
Данная публикация является личным мнением автора. Мнение владельца сайта может не совпадать с мнением автора.
Конкретизируй вопрос: хочешь получить формализованную оценку достаточной глубины ретроспективы для обучения модели? Верно понял вопрос? Если верно, то ответ зависит от «метода обучения» или способа расчетов.
В правильно заданом вопросе есть уже половина ответа и это более-менее понятно:) Дело тут даже не в оценке, ведь чтобы ее получить — надо:
a) сперва что-то с чем-то сравнить. И это, на мой взгляд, больше похоже на эмпирический метод.
b) потратить время на перебор вариантов.
Это всё допустимо, но наверное можно попытаться вычислить шаг до получения этой оценки — как-то аналитически и «заранее» посчитать длину этой ретроспективы опираясь на характеристики самих данных![]()
Пофантазирую: к примеру, ты ищешь некий паттерн некой закономерности. Задал глубину ретроспективы N, не нашел, увеличиваешь глубину, пока не нашел на глубине N1. Хватит? Нет, поскольку следует найти более одного подтверждения работы паттерна. Сколько — зависит от твоей ТС и взгляда. В общем случае лучше не ограничиваться одним паттерном, чтобы избежать подгонки, а иметь несколько. Для расчетного метода идея другая.
Ответ на вскидку, не особо кубатуря в проблеме. Да и проблема туманна....
Можно и паттернами мерять, можно твоими величинами, думаю принцип должен быть похожим. Если паттернами, тогда, что если на некой глубине ретроспективы встречается N-повторений одного паттерна, допустим 20 или 120 и статистика вроде как набрана. Вот тут, чтобы не получить переобучение или наоборот шум на выходе, как понять, а в идеале вычислить заранее и без перебора, этих N уже хватит или еще недостаточно? (если отталкиваться от того, что глубина задается N-повторениями одного паттерна, но мне эта идея не очень). Но в целом твоя идея понятна, она +- похожа на мою. Ладно, буду думать, это единственно нерешенная задача осталась в этой чертовщине :)
«следует иметь статистику иное сочетание параметров -> такое же движение цены» — а это уже другой паттерн и задача свелась к ранжированию разных паттернов на одной выборке. Если допустим ты предполагаешь что к некому паттерну можно добавить скрытый параметр, вроде показания объема или индикатора, то по сути получаем новый патерн, с теми же проблемами статистики недобора или перебора и вопрос с длиной ретроспективы так же остатся открытым.
В твоем методе проще только потому, что модель не учитывает динамику паттерна, а только лишь форму патерна (взаимосвязи величин), попробуй добавь поведение и все тут же поменяется, столкнешься с точно такой же проблемой.
Что касается моего подхода — у меня нет понятия «динамика паттерна», впрочем и паттерна как такового нет. Абсолютно численные решения с четкой формализацией количественных оценок «сочетания параметров». Поэтому я не заморачивался подобной проблемой. Для меня вопрос был найти глубину ретроспективы, на которой сохраняется относительная стабильность результата и его масштабируемость по активам. Я это решил давно. В плане динамики есть проблема с существенно сильными трендами и ярко выраженными трендовыми активами. Это проблема моего подхода в целом, поскольку указанные выше случаи имеют сильную нелинейность, и там просто иное численное решение надо применять. Но как не смешно, основная проблема не в изменении метода, а в определении, что такой момент наступил. Это аналогично вопросу определения тренда. Можно сделать только в определенном пост-факте.
Вот тупой прямолинейный пример, пришедший на вскидку: сочетание Close>Open, ожидаем CloseNext>Close — это и критерий движения (твое упрощенное вверх/вниз). Что и как делаем: разбиваем ретроспективу на 2 выборки: CloseNext>Close и CloseNext<Close. Если сочетание подтверждается в первой выборке и не опровергается во второй — можно считать, что оно работает. И не требуется тут полной „оцифровки“ иных сочетаний (например, High=Open ...). Со стороны „сочетаний“ ограничиваемся классификацией „наше сочетание“/»не наше сочетание".
Тренировку и тесты ввиду совсем не имел.
Метод заключается в анализе сходимости функции ошибки при вариации длины ретроспективы. Проводятся итерационные замеры на скользящих окнах с возрастающим шагом. Оптимальным значением n_bars считается начало устойчивого плато, где производная функции ошибки по длине окна стремится к нулю. Это точка, после которой включение дополнительных исторических данных уже не приносит новой информации (информационное насыщение), а лишь увеличивает вычислительную нагрузку или размывает актуальные закономерности.
В твоей модели глубина n_bars, вероятно, — константа, подобранная как некая универсальная величина. Это популярный метод, и он подходит большинству стратегий, но универсальность удобна до поры до времени. Ты сам подтверждаешь, что модель не умеет отрабатывать резкую смену волатильности — и это прямое следствие неоптимальной ретроспективы. Из-за фиксированного окна модели банально не хватает данных для адаптации к аномалиям и попытка компенсировать это системой из трех окон выглядит скорее как тактический обход проблемы, чем ее фундаментальное решение.
«при таком определении оптимальной глубины ретроспективы есть риск получить подгонку модели под текущий-прошедший характер движения цены»
Ну, подгонкой это назвать можно, но только в самом широком смысле.
Это ближе к структурной калибровке, адаптации, а не к оптимизации под доходность. Не ищется n_back, который максимизирует прибыль или точность на тестовой выборке. Ищется минимальное окно, при котором модель стабильно идентифицирует свою же внутреннюю структуру. Это ближе к вопросу «сколько данных нужно модели, чтобы работать корректно», чем к «на каких данных модель давала лучшие сигналы».
Пока ничего лучшего не нашел, приходится «мутить Франкенштейна»