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