Блог им. NatashaMe




MOEX DATA DOWNLOADER — ПОЛНАЯ ДОКУМЕНТАЦИЯ
📋 ОБЩЕЕ ОПИСАНИЕ
MOEX Data Downloader — это специализированное приложение для загрузки, обновления и обработки исторических биржевых данных с Московской биржи (MOEX). Программа предоставляет удобный графический интерфейс для работы с котировками акций.
🚀 АЛГОРИТМ РАБОТЫ ПРИЛОЖЕНИЯ
Основной принцип:
1. Первоначальная загрузка → Регулярное обновление → Генерация таймфреймов (при необходимости)
2. Все таймфреймы (кроме 1 минуты) генерируются из минутных данных
3. Для обновления данных необходимы файлы *_1min.csv (или *_1min.txt)Детальный процесс:
1. ЗАГРУЗКА ДАННЫХ (вкладка «Загрузка данных»)
— Загружаются 1-минутные данные с MOEX API
— Автоматически создаются выбранные таймфреймы на основе минутных данных
— Сохраняются файлы в формате: ТИКЕР_ТАЙМФРЕЙМ.расширение2. ОБНОВЛЕНИЕ ДАННЫХ (вкладка «Обновление данных»)
— Сканируются существующие минутные файлы (*_1min.*)
— Определяется последняя доступная дата для каждого тикера
— Загружаются только новые данные с даты, следующей за последней
— Автоматически обновляются все существующие таймфреймы3. ГЕНЕРАЦИЯ ТАЙМФРЕЙМОВ (ДОПОЛНИТЕЛЬНАЯ ФУНКЦИЯ)
— Необязательная операция — создание недостающих таймфреймов
— Используется, если нужно добавить таймфреймы к уже существующим данным
— Рекомендуется создавать все нужные таймфреймы при первоначальной загрузке🏗️ АЛГОРИТМ ПРЕОБРАЗОВАНИЯ ТАЙМФРЕЙМОВ
Из 1-минутных данных создаются:
Таймфрейм | Правило формирования
------------|---------------------
5 минут | Группировка по 5 минутным интервалам
15 минут | Группировка по 15 минутным интервалам
1 час | Группировка по часовым интервалам
4 часа | Группировка по 4-часовым интервалам
1 день | Группировка по дневным интерваламФормирование свечей:
— OPEN — цена открытия первой свечи в интервале
— HIGH — максимальная цена из всех свечей интервала
— LOW — минимальная цена из всех свечей интервала
— CLOSE — цена закрытия последней свечи интервала
— VOLUME — суммарный объем всех свечей интервала📊 ОПИСАНИЕ ИНТЕРФЕЙСА
ВКЛАДКА «ЗАГРУЗКА ДАННЫХ»
Поля ввода:
— Тикеры — список тикеров через запятую (кнопка "..." для выбора из списка)
*По умолчанию загружаются тикеры из индекса IMOEX через API MOEX:*
*API вызов: iss.moex.com/iss/statistics/engines/stock/markets/index/analytics/IMOEX.json*
*Это обеспечивает актуальный список ликвидных акций*
— Начальная дата — дата начала загрузки в формате ГГГГ-ММ-ДД
— Конечная дата — дата окончания загрузки
— Папка для сохранения — путь для сохранения файловНастройки:
— Таймфреймы — выбор временных интервалов для создания
— Формат файлов — CSV (с угловыми скобками) или TXT (без скобок)
— Только будни — исключать выходные дни из загрузкиКнопки:
— "..." (возле тикеров) — открывает окно выбора тикеров из списка MOEX
— «Обзор» — выбор папки для сохранения
— «СКАЧАТЬ ДАННЫЕ» — запуск процесса загрузкиИндикаторы:
— Общий прогресс — прогресс по всем тикерам и таймфреймам
— Прогресс загрузки дня — прогресс загрузки текущего дня
— Лог выполнения — подробный журнал операций
— Статусная строка — текущее состояние приложенияВКЛАДКА «ОБНОВЛЕНИЕ ДАННЫХ»
Поля ввода:
— Папка с данными — путь к папке с ранее загруженными данными
— Обновить до даты — дата, до которой обновлять данныеКнопки:
— "..." — выбор папки с данными
— «Сканировать файлы» — обновление списка тикеров
— «ОБНОВИТЬ» — запуск обновления выбранных тикеров
— «ГЕНЕРИРОВАТЬ» — создание недостающих таймфреймов (дополнительная функция)Таблица тикеров:
— Выбрать — чекбокс для выбора тикера для обновления
— Тикер — название тикера
— Последняя дата — дата последней доступной свечи
— Кол-во свечей — общее количество свечей в файле
— Таймфреймы — список доступных таймфреймовИндикаторы:
— Прогресс обновления — общий прогресс обновления
— Лог обновления — журнал операций обновления🔧 ТЕХНИЧЕСКИЕ ДЕТАЛИ ЗАГРУЗКИ
Почему данные загружаются порциями?
MOEX API имеет ограничение на количество возвращаемых свечей (максимум 500 свечей за один запрос). Поэтому для загрузки полного дня данных применяется алгоритм последовательных запросов:Пример загрузки данных за один день:
Первый запрос: с 09:30:00 до [время последней свечи + 1 минута]
Второй запрос: с [время последней свечи] до конца дня
и т.д., пока не будут получены все свечи дняКонкретный API вызов:
url = «iss.moex.com/iss/engines/stock/markets/shares/securities/GAZP/candles.json»
params = {
'from': '2024-01-15 09:30:00',
'till': '2024-01-15 18:45:00',
'interval': 1,
'iss.meta': 'off'
}Этот подход обеспечивает:
✅ Полноту данных (все минутные свечи за день)
✅ Обход ограничений API
✅ Стабильность загрузки при большом объеме данных💾 ФОРМАТЫ ФАЙЛОВ
CSV формат (с угловыми скобками):
<DATE>,<TIME>,<OPEN>,<HIGH>,<LOW>,<CLOSE>,<VOL>
20231201,100000,315.5000000,315.8000000,315.2000000,315.6000000,15000TXT формат (табуляция):
DATE TIME OPEN HIGH LOW CLOSE VOL
20231201 100000 315.5000000 315.8000000 315.2000000 315.6000000 15000🎯 РЕКОМЕНДАЦИИ ПО ИСПОЛЬЗОВАНИЮ
Для новых пользователей:
1. Начните с вкладки «Загрузка данных»
2. Выберите тикеры через кнопку "..." (рекомендуется использовать список IMOEX)
3. Укажите период загрузки (рекомендуется 30-60 дней для начала)
4. Выберите все нужные таймфреймы сразу
5. Нажмите «СКАЧАТЬ ДАННЫЕ»Для регулярного использования:
1. Используйте вкладку «Обновление данных»
2. Укажите папку с ранее загруженными данными
3. Нажмите «Сканировать файлы»
4. Выберите тикеры для обновления
5. Укажите дату обновления и нажмите «ОБНОВИТЬ»При добавлении новых таймфреймов (редкий случай):
1. Перейдите на вкладку «Обновление данных»
2. Нажмите «ГЕНЕРИРОВАТЬ»
3. Выберите нужные таймфреймы
4. Нажмите «Сгенерировать»⚠️ ВАЖНЫЕ ЗАМЕЧАНИЯ
— Для работы обновления необходимы файлы *_1min.*
— Все таймфреймы (кроме 1min) создаются из минутных данных
— При обновлении данных автоматически обновляются все существующие таймфреймы
— Рекомендуется использовать формат CSV для лучшей совместимости
— При первом использовании создается папка 'out' в директории программы
— Загрузка данных происходит порциями из-за ограничений MOEX API (500 свечей за запрос)---
Версия программы: 1.0
Автор: t.me/GeorgyBlog
---
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import pandas as pd
import requests
import os
from datetime import datetime, timedelta
import logging
import time
import threading
import glob
import numpy as np
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class MoexDataDownloader:
def __init__(self, root):
self.root = root
# Загружаем тикеры с MOEX при инициализации
self.imoex_tickers = self.load_initial_tickers()
self.downloading = False
self.current_ticker = ""
self.ticker_vars = {} # Для хранения чекбоксов тикеров
self.selected_tickers_state = {} # Для сохранения состояния выделения тикеров
# Создаем папку out если ее нет
self.default_out_path = os.path.join(os.getcwd(), 'out')
os.makedirs(self.default_out_path, exist_ok=True)
self.setup_ui()
# Автоматическое сканирование при открытии вкладки
self.notebook.bind('<<NotebookTabChanged>>', self.on_tab_changed)
def load_initial_tickers(self):
"""Загружает начальные тикеры с MOEX"""
try:
url = "https://iss.moex.com/iss/statistics/engines/stock/markets/index/analytics/IMOEX.json?iss.meta=off&limit=9999"
response = requests.get(url, timeout=10)
if response.status_code == 200:
data = response.json()
tickers_data = data['analytics']['data']
# Берем только тикеры из данных
tickers = list(set([ticker_info[2] for ticker_info in tickers_data]))
tickers.sort()
return tickers
else:
# Если не удалось загрузить, используем дефолтный список
return ['SBER', 'GAZP', 'LKOH', 'GMKN', 'ROSN', 'NVTK', 'TATN', 'MTSS',
'MGNT', 'SNGS', 'SNGSP', 'CHMF', 'PLZL', 'ALRS', 'MOEX', 'VTBR',
'PHOR', 'RUAL', 'AFKS', 'PIKK', 'TCSG', 'POLY', 'YNDX', 'OZON']
except Exception as e:
print(f"Ошибка загрузки тикеров: {e}")
return ['SBER', 'GAZP', 'LKOH', 'GMKN', 'ROSN', 'NVTK', 'TATN', 'MTSS',
'MGNT', 'SNGS', 'SNGSP', 'CHMF', 'PLZL', 'ALRS', 'MOEX', 'VTBR',
'PHOR', 'RUAL', 'AFKS', 'PIKK', 'TCSG', 'POLY', 'YNDX', 'OZON']
def on_tab_changed(self, event):
"""Автоматическое сканирование при открытии вкладки обновления"""
current_tab = self.notebook.tab(self.notebook.select(), "text")
if current_tab == "Обновление данных":
# Небольшая задержка чтобы интерфейс успел отобразиться
self.root.after(100, self.scan_files)
def setup_ui(self):
self.root.title("MOEX Data Downloader - Скачивание данных Московской биржи")
self.root.geometry("1600x1200")
# Центрируем окно на экране
self.root.update_idletasks()
x = (self.root.winfo_screenwidth() - 1600) // 2
y = (self.root.winfo_screenheight() - 1200) // 2
self.root.geometry(f"1600x1200+{x}+{y}")
# Создаем Notebook (вкладки)
self.notebook = ttk.Notebook(self.root)
self.notebook.pack(fill='both', expand=True, padx=10, pady=10)
# Вкладка 1: Загрузка данных
self.download_tab = ttk.Frame(self.notebook)
self.notebook.add(self.download_tab, text='Загрузка данных')
# Вкладка 2: Обновление данных
self.update_tab = ttk.Frame(self.notebook)
self.notebook.add(self.update_tab, text='Обновление данных')
# Вкладка 3: Документация
self.docs_tab = ttk.Frame(self.notebook)
self.notebook.add(self.docs_tab, text='Документация')
# Настраиваем вкладки
self.setup_download_tab()
self.setup_update_tab()
self.setup_docs_tab()
def setup_download_tab(self):
"""Настройка вкладки загрузки данных"""
# Автоматические даты
end_date = datetime.now().strftime('%Y-%m-%d')
start_date = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d')
# Основной фрейм для вкладки загрузки
main_frame = ttk.Frame(self.download_tab, padding="20")
main_frame.pack(fill='both', expand=True)
# Настройка весов для растягивания
main_frame.columnconfigure(1, weight=1)
main_frame.rowconfigure(11, weight=1)
# Поля ввода - исправленная версия с правильной компоновкой
ttk.Label(main_frame, text="Тикеры (через запятую):", font=('Arial', 12)).grid(row=0, column=0, sticky='w', padx=10, pady=10)
ticker_input_frame = ttk.Frame(main_frame)
ticker_input_frame.grid(row=0, column=1, columnspan=2, sticky='we', padx=10, pady=10)
ticker_input_frame.columnconfigure(0, weight=1)
self.tickers_entry = ttk.Entry(ticker_input_frame, font=('Arial', 11))
self.tickers_entry.insert(0, ", ".join(self.imoex_tickers))
self.tickers_entry.grid(row=0, column=0, sticky='we', padx=(0, 10))
# Кнопка для выбора тикеров
ttk.Button(ticker_input_frame, text="...", command=self.show_ticker_selection, width=3).grid(row=0, column=1)
ttk.Label(main_frame, text="Начальная дата (ГГГГ-ММ-ДД):", font=('Arial', 12)).grid(row=1, column=0, sticky='w', padx=10, pady=10)
self.start_entry = ttk.Entry(main_frame, width=25, font=('Arial', 11))
self.start_entry.insert(0, start_date)
self.start_entry.grid(row=1, column=1, padx=10, pady=10, sticky='w')
ttk.Label(main_frame, text="Конечная дата (ГГГГ-ММ-ДД):", font=('Arial', 12)).grid(row=2, column=0, sticky='w', padx=10, pady=10)
self.end_entry = ttk.Entry(main_frame, width=25, font=('Arial', 11))
self.end_entry.insert(0, end_date)
self.end_entry.grid(row=2, column=1, padx=10, pady=10, sticky='w')
ttk.Label(main_frame, text="Папка для сохранения:", font=('Arial', 12)).grid(row=3, column=0, sticky='w', padx=10, pady=10)
self.path_entry = ttk.Entry(main_frame, width=100, font=('Arial', 11))
self.path_entry.insert(0, self.default_out_path)
self.path_entry.grid(row=3, column=1, padx=10, pady=10, sticky='we')
ttk.Button(main_frame, text="Обзор", command=self.browse_folder, width=15).grid(row=3, column=2, padx=10, pady=10)
# Чекбоксы для таймфреймов
ttk.Label(main_frame, text="Таймфреймы:", font=('Arial', 12, 'bold')).grid(row=4, column=0, sticky='nw', padx=10, pady=10)
self.timeframe_frame = tk.Frame(main_frame)
self.timeframe_frame.grid(row=4, column=1, columnspan=2, sticky='w', padx=10, pady=10)
self.var_1min = tk.BooleanVar(value=True)
self.var_5min = tk.BooleanVar(value=True)
self.var_15min = tk.BooleanVar(value=True)
self.var_1h = tk.BooleanVar(value=True)
self.var_4h = tk.BooleanVar(value=True)
self.var_1d = tk.BooleanVar(value=True)
frame_row1 = tk.Frame(self.timeframe_frame)
frame_row1.pack(fill='x', pady=3)
tk.Checkbutton(frame_row1, text="1 минута", variable=self.var_1min, font=('Arial', 11),
width=12, anchor='w').pack(side='left', padx=15)
tk.Checkbutton(frame_row1, text="5 минут", variable=self.var_5min, font=('Arial', 11),
width=12, anchor='w').pack(side='left', padx=15)
tk.Checkbutton(frame_row1, text="15 минут", variable=self.var_15min, font=('Arial', 11),
width=12, anchor='w').pack(side='left', padx=15)
tk.Checkbutton(frame_row1, text="1 час", variable=self.var_1h, font=('Arial', 11),
width=12, anchor='w').pack(side='left', padx=15)
frame_row2 = tk.Frame(self.timeframe_frame)
frame_row2.pack(fill='x', pady=3)
tk.Checkbutton(frame_row2, text="4 часа", variable=self.var_4h, font=('Arial', 11),
width=12, anchor='w').pack(side='left', padx=15)
tk.Checkbutton(frame_row2, text="1 день", variable=self.var_1d, font=('Arial', 11),
width=12, anchor='w').pack(side='left', padx=15)
# Выбор формата файла
ttk.Label(main_frame, text="Формат файлов:", font=('Arial', 12, 'bold')).grid(row=5, column=0, sticky='w', padx=10, pady=10)
self.format_frame = tk.Frame(main_frame)
self.format_frame.grid(row=5, column=1, columnspan=2, sticky='w', padx=10, pady=10)
self.file_format = tk.StringVar(value="csv")
tk.Radiobutton(self.format_frame, text="CSV", variable=self.file_format,
value="csv", font=('Arial', 11)).pack(side='left', padx=15)
tk.Radiobutton(self.format_frame, text="TXT", variable=self.file_format,
value="txt", font=('Arial', 11)).pack(side='left', padx=15)
# Оптимизация скорости
ttk.Label(main_frame, text="Режим загрузки:", font=('Arial', 12, 'bold')).grid(row=6, column=0, sticky='w', padx=10, pady=10)
self.optimize_frame = tk.Frame(main_frame)
self.optimize_frame.grid(row=6, column=1, columnspan=2, sticky='w', padx=10, pady=10)
self.only_weekdays = tk.BooleanVar(value=True)
tk.Checkbutton(self.optimize_frame, text="Качать данные только по будням",
variable=self.only_weekdays, font=('Arial', 11)).pack(side='left', padx=15)
# Кнопка загрузки
button_frame = tk.Frame(main_frame)
button_frame.grid(row=7, column=0, columnspan=3, pady=20)
self.download_btn = tk.Button(button_frame, text="СКАЧАТЬ ДАННЫЕ", command=self.start_download_thread,
font=('Arial', 12, 'bold'), width=20, height=2,
bg='#0078D7', fg='white', relief='raised', bd=3)
self.download_btn.pack(side='left', padx=20)
# Прогресс бары
ttk.Label(main_frame, text="Общий прогресс:", font=('Arial', 11)).grid(row=8, column=0, sticky='w', padx=10, pady=5)
self.progress = ttk.Progressbar(main_frame, orient='horizontal', length=1400, mode='determinate')
self.progress.grid(row=8, column=1, columnspan=2, padx=10, pady=5, sticky='we')
ttk.Label(main_frame, text="Прогресс загрузки дня:", font=('Arial', 11)).grid(row=9, column=0, sticky='w', padx=10, pady=5)
self.day_progress = ttk.Progressbar(main_frame, orient='horizontal', length=1400, mode='determinate')
self.day_progress.grid(row=9, column=1, columnspan=2, padx=10, pady=5, sticky='we')
# Лог
ttk.Label(main_frame, text="Лог выполнения:", font=('Arial', 12, 'bold')).grid(row=10, column=0, sticky='w', padx=10, pady=10)
log_frame = ttk.Frame(main_frame)
log_frame.grid(row=11, column=0, columnspan=3, padx=10, pady=10, sticky='nsew')
log_frame.columnconfigure(0, weight=1)
log_frame.rowconfigure(0, weight=1)
self.log_text = tk.Text(log_frame, height=25, width=150, font=('Consolas', 10))
self.log_text.grid(row=0, column=0, sticky='nsew')
scrollbar_y = ttk.Scrollbar(log_frame, command=self.log_text.yview)
scrollbar_y.grid(row=0, column=1, sticky='ns')
scrollbar_x = ttk.Scrollbar(log_frame, command=self.log_text.xview, orient='horizontal')
scrollbar_x.grid(row=1, column=0, sticky='ew')
self.log_text.config(yscrollcommand=scrollbar_y.set, xscrollcommand=scrollbar_x.set)
# Статус бар
self.status_var = tk.StringVar(value="Готов к работе")
status_bar = ttk.Label(main_frame, textvariable=self.status_var, relief='sunken', anchor='w', font=('Arial', 10))
status_bar.grid(row=12, column=0, columnspan=3, sticky='we', padx=10, pady=10)
def setup_update_tab(self):
"""Настройка вкладки обновления данных"""
main_frame = ttk.Frame(self.update_tab, padding="20")
main_frame.pack(fill='both', expand=True)
# Верхняя панель с настройками
settings_frame = ttk.Frame(main_frame)
settings_frame.pack(fill='x', pady=(0, 20))
# Строка 1: Папка с данными
folder_frame = ttk.Frame(settings_frame)
folder_frame.pack(fill='x', pady=(0, 10))
ttk.Label(folder_frame, text="Папка с данными:", font=('Arial', 12)).pack(side='left', padx=(0, 10))
self.update_path_entry = ttk.Entry(folder_frame, width=70, font=('Arial', 11))
self.update_path_entry.insert(0, self.default_out_path)
self.update_path_entry.pack(side='left', fill='x', expand=True, padx=(0, 10))
ttk.Button(folder_frame, text="...", command=self.browse_update_folder, width=5).pack(side='left', padx=(0, 10))
# Строка 2: Дата обновления и кнопки
date_buttons_frame = ttk.Frame(settings_frame)
date_buttons_frame.pack(fill='x', pady=(0, 10))
ttk.Label(date_buttons_frame, text="Обновить до даты:", font=('Arial', 12)).pack(side='left', padx=(0, 10))
self.update_end_date = ttk.Entry(date_buttons_frame, width=15, font=('Arial', 11))
self.update_end_date.insert(0, datetime.now().strftime('%Y-%m-%d'))
self.update_end_date.pack(side='left', padx=(0, 20))
ttk.Button(date_buttons_frame, text="Сканировать файлы", command=self.scan_files,
width=20).pack(side='left', padx=(0, 10))
self.update_btn = tk.Button(date_buttons_frame, text="ОБНОВИТЬ", command=self.start_update_thread,
font=('Arial', 12, 'bold'), width=15, height=1,
bg='#28A745', fg='white', relief='raised', bd=3)
self.update_btn.pack(side='left', padx=(0, 10))
# Кнопка генерации таймфреймов
self.generate_btn = tk.Button(date_buttons_frame, text="ГЕНЕРИРОВАТЬ", command=self.show_generate_timeframes,
font=('Arial', 12, 'bold'), width=15, height=1,
bg='#FF9800', fg='white', relief='raised', bd=3)
self.generate_btn.pack(side='left')
# Фрейм для списка тикеров
list_frame = ttk.Frame(main_frame)
list_frame.pack(fill='both', expand=True)
# Заголовки таблицы
header_frame = ttk.Frame(list_frame)
header_frame.pack(fill='x', pady=(0, 5))
ttk.Label(header_frame, text="Выбрать", font=('Arial', 11, 'bold'), width=8).pack(side='left', padx=(20, 10))
ttk.Label(header_frame, text="Тикер", font=('Arial', 11, 'bold'), width=15).pack(side='left', padx=10)
ttk.Label(header_frame, text="Последняя дата", font=('Arial', 11, 'bold'), width=20).pack(side='left', padx=10)
ttk.Label(header_frame, text="Кол-во свечей", font=('Arial', 11, 'bold'), width=15).pack(side='left', padx=10)
ttk.Label(header_frame, text="Таймфреймы", font=('Arial', 11, 'bold'), width=20).pack(side='left', padx=10)
# Создаем скроллируемый фрейм для списка тикеров
canvas = tk.Canvas(list_frame, bg='white')
scrollbar = ttk.Scrollbar(list_frame, orient="vertical", command=canvas.yview)
self.scrollable_frame = ttk.Frame(canvas)
self.scrollable_frame.bind(
"<Configure>",
lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
)
canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set)
canvas.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
# Прогресс бар для обновления
ttk.Label(main_frame, text="Прогресс обновления:", font=('Arial', 11)).pack(anchor='w', pady=(20, 5))
self.update_progress = ttk.Progressbar(main_frame, orient='horizontal', mode='determinate')
self.update_progress.pack(fill='x', pady=(0, 10))
# Лог обновления
ttk.Label(main_frame, text="Лог обновления:", font=('Arial', 12, 'bold')).pack(anchor='w', pady=(10, 5))
update_log_frame = ttk.Frame(main_frame)
update_log_frame.pack(fill='both', expand=True)
self.update_log_text = tk.Text(update_log_frame, height=15, width=150, font=('Consolas', 10))
self.update_log_text.pack(side='left', fill='both', expand=True)
update_scrollbar = ttk.Scrollbar(update_log_frame, command=self.update_log_text.yview)
update_scrollbar.pack(side='right', fill='y')
self.update_log_text.config(yscrollcommand=update_scrollbar.set)
def setup_docs_tab(self):
"""Настройка вкладки документации"""
docs_frame = ttk.Frame(self.docs_tab)
docs_frame.pack(fill='both', expand=True)
text_frame = ttk.Frame(docs_frame)
text_frame.pack(fill='both', expand=True, padx=10, pady=10)
docs_text = tk.Text(text_frame, wrap='word', font=('Arial', 11), padx=10, pady=10)
docs_text.pack(side='left', fill='both', expand=True)
scrollbar = ttk.Scrollbar(text_frame, command=docs_text.yview)
scrollbar.pack(side='right', fill='y')
docs_text.config(yscrollcommand=scrollbar.set)
documentation = self.get_documentation()
docs_text.insert('1.0', documentation)
docs_text.config(state='disabled')
def get_documentation(self):
"""Возвращает текстовую документацию"""
return """
MOEX DATA DOWNLOADER - ПОЛНАЯ ДОКУМЕНТАЦИЯ
📋 ОБЩЕЕ ОПИСАНИЕ
MOEX Data Downloader - это специализированное приложение для загрузки, обновления и обработки исторических биржевых данных с Московской биржи (MOEX). Программа предоставляет удобный графический интерфейс для работы с котировками акций.
🚀 АЛГОРИТМ РАБОТЫ ПРИЛОЖЕНИЯ
Основной принцип:
1. Первоначальная загрузка → Регулярное обновление → Генерация таймфреймов (при необходимости)
2. Все таймфреймы (кроме 1 минуты) генерируются из минутных данных
3. Для обновления данных необходимы файлы *_1min.csv (или *_1min.txt)
Детальный процесс:
1. ЗАГРУЗКА ДАННЫХ (вкладка "Загрузка данных")
- Загружаются 1-минутные данные с MOEX API
- Автоматически создаются выбранные таймфреймы на основе минутных данных
- Сохраняются файлы в формате: ТИКЕР_ТАЙМФРЕЙМ.расширение
2. ОБНОВЛЕНИЕ ДАННЫХ (вкладка "Обновление данных")
- Сканируются существующие минутные файлы (*_1min.*)
- Определяется последняя доступная дата для каждого тикера
- Загружаются только новые данные с даты, следующей за последней
- Автоматически обновляются все существующие таймфреймы
3. ГЕНЕРАЦИЯ ТАЙМФРЕЙМОВ (ДОПОЛНИТЕЛЬНАЯ ФУНКЦИЯ)
- Необязательная операция - создание недостающих таймфреймов
- Используется, если нужно добавить таймфреймы к уже существующим данным
- Рекомендуется создавать все нужные таймфреймы при первоначальной загрузке
🏗️ АЛГОРИТМ ПРЕОБРАЗОВАНИЯ ТАЙМФРЕЙМОВ
Из 1-минутных данных создаются:
Таймфрейм | Правило формирования
------------|---------------------
5 минут | Группировка по 5 минутным интервалам
15 минут | Группировка по 15 минутным интервалам
1 час | Группировка по часовым интервалам
4 часа | Группировка по 4-часовым интервалам
1 день | Группировка по дневным интервалам
Формирование свечей:
- OPEN - цена открытия первой свечи в интервале
- HIGH - максимальная цена из всех свечей интервала
- LOW - минимальная цена из всех свечей интервала
- CLOSE - цена закрытия последней свечи интервала
- VOLUME - суммарный объем всех свечей интервала
📊 ОПИСАНИЕ ИНТЕРФЕЙСА
ВКЛАДКА "ЗАГРУЗКА ДАННЫХ"
Поля ввода:
- Тикеры - список тикеров через запятую (кнопка "..." для выбора из списка)
*По умолчанию загружаются тикеры из индекса IMOEX через API MOEX:*
*API вызов: https://iss.moex.com/iss/statistics/engines/stock/markets/index/analytics/IMOEX.json*
*Это обеспечивает актуальный список ликвидных акций*
- Начальная дата - дата начала загрузки в формате ГГГГ-ММ-ДД
- Конечная дата - дата окончания загрузки
- Папка для сохранения - путь для сохранения файлов
Настройки:
- Таймфреймы - выбор временных интервалов для создания
- Формат файлов - CSV (с угловыми скобками) или TXT (без скобок)
- Только будни - исключать выходные дни из загрузки
Кнопки:
- "..." (возле тикеров) - открывает окно выбора тикеров из списка MOEX
- "Обзор" - выбор папки для сохранения
- "СКАЧАТЬ ДАННЫЕ" - запуск процесса загрузки
Индикаторы:
- Общий прогресс - прогресс по всем тикерам и таймфреймам
- Прогресс загрузки дня - прогресс загрузки текущего дня
- Лог выполнения - подробный журнал операций
- Статусная строка - текущее состояние приложения
ВКЛАДКА "ОБНОВЛЕНИЕ ДАННЫХ"
Поля ввода:
- Папка с данными - путь к папке с ранее загруженными данными
- Обновить до даты - дата, до которой обновлять данные
Кнопки:
- "..." - выбор папки с данными
- "Сканировать файлы" - обновление списка тикеров
- "ОБНОВИТЬ" - запуск обновления выбранных тикеров
- "ГЕНЕРИРОВАТЬ" - создание недостающих таймфреймов (дополнительная функция)
Таблица тикеров:
- Выбрать - чекбокс для выбора тикера для обновления
- Тикер - название тикера
- Последняя дата - дата последней доступной свечи
- Кол-во свечей - общее количество свечей в файле
- Таймфреймы - список доступных таймфреймов
Индикаторы:
- Прогресс обновления - общий прогресс обновления
- Лог обновления - журнал операций обновления
🔧 ТЕХНИЧЕСКИЕ ДЕТАЛИ ЗАГРУЗКИ
Почему данные загружаются порциями?
MOEX API имеет ограничение на количество возвращаемых свечей (максимум 500 свечей за один запрос). Поэтому для загрузки полного дня данных применяется алгоритм последовательных запросов:
Пример загрузки данных за один день:
Первый запрос: с 09:30:00 до [время последней свечи + 1 минута]
Второй запрос: с [время последней свечи] до конца дня
и т.д., пока не будут получены все свечи дня
Конкретный API вызов:
url = "http://iss.moex.com/iss/engines/stock/markets/shares/securities/GAZP/candles.json"
params = {
'from': '2024-01-15 09:30:00',
'till': '2024-01-15 18:45:00',
'interval': 1,
'iss.meta': 'off'
}
Этот подход обеспечивает:
✅ Полноту данных (все минутные свечи за день)
✅ Обход ограничений API
✅ Стабильность загрузки при большом объеме данных
💾 ФОРМАТЫ ФАЙЛОВ
CSV формат (с угловыми скобками):
<DATE>,<TIME>,<OPEN>,<HIGH>,<LOW>,<CLOSE>,<VOL>
20231201,100000,315.5000000,315.8000000,315.2000000,315.6000000,15000
TXT формат (табуляция):
DATE TIME OPEN HIGH LOW CLOSE VOL
20231201 100000 315.5000000 315.8000000 315.2000000 315.6000000 15000
🎯 РЕКОМЕНДАЦИИ ПО ИСПОЛЬЗОВАНИЮ
Для новых пользователей:
1. Начните с вкладки "Загрузка данных"
2. Выберите тикеры через кнопку "..." (рекомендуется использовать список IMOEX)
3. Укажите период загрузки (рекомендуется 30-60 дней для начала)
4. Выберите все нужные таймфреймы сразу
5. Нажмите "СКАЧАТЬ ДАННЫЕ"
Для регулярного использования:
1. Используйте вкладку "Обновление данных"
2. Укажите папку с ранее загруженными данными
3. Нажмите "Сканировать файлы"
4. Выберите тикеры для обновления
5. Укажите дату обновления и нажмите "ОБНОВИТЬ"
При добавлении новых таймфреймов (редкий случай):
1. Перейдите на вкладку "Обновление данных"
2. Нажмите "ГЕНЕРИРОВАТЬ"
3. Выберите нужные таймфреймы
4. Нажмите "Сгенерировать"
⚠️ ВАЖНЫЕ ЗАМЕЧАНИЯ
- Для работы обновления необходимы файлы *_1min.*
- Все таймфреймы (кроме 1min) создаются из минутных данных
- При обновлении данных автоматически обновляются все существующие таймфреймы
- Рекомендуется использовать формат CSV для лучшей совместимости
- При первом использовании создается папка 'out' в директории программы
- Загрузка данных происходит порциями из-за ограничений MOEX API (500 свечей за запрос)
---
Версия программы: 1.0
Автор: https://t.me/GeorgyBlog
---
"""
def show_generate_timeframes(self):
"""Показывает окно генерации таймфреймов"""
# Получаем выбранные тикеры
selected_tickers = []
for ticker, data in self.ticker_vars.items():
if data['var'].get():
selected_tickers.append(ticker)
if not selected_tickers:
messagebox.showwarning("Внимание", "Выберите тикеры для генерации таймфреймов")
return
selection_window = tk.Toplevel(self.root)
selection_window.title("Генерация таймфреймов")
selection_window.geometry("1200x750")
selection_window.transient(self.root)
selection_window.grab_set()
# Центрируем окно
selection_window.update_idletasks()
x = (self.root.winfo_screenwidth() - 1200) // 2
y = (self.root.winfo_screenheight() - 750) // 2
selection_window.geometry(f"1200x750+{x}+{y}")
main_frame = ttk.Frame(selection_window, padding="20")
main_frame.pack(fill='both', expand=True)
ttk.Label(main_frame, text="Выберите таймфреймы для генерации:",
font=('Arial', 14, 'bold')).pack(anchor='w', pady=(0, 20))
# Фрейм для чекбоксов таймфреймов
timeframe_frame = tk.Frame(main_frame)
timeframe_frame.pack(fill='x', pady=(0, 20))
# Переменные для чекбоксов - УБИРАЕМ 1min
var_5min = tk.BooleanVar(value=True)
var_15min = tk.BooleanVar(value=True)
var_1h = tk.BooleanVar(value=True)
var_4h = tk.BooleanVar(value=True)
var_1d = tk.BooleanVar(value=True)
# Первый ряд чекбоксов (3 таймфрейма вместо 4)
frame_row1 = tk.Frame(timeframe_frame)
frame_row1.pack(fill='x', pady=15)
# УБИРАЕМ ЧЕКБОКС "1 минута"
tk.Checkbutton(frame_row1, text="5 минут", variable=var_5min, font=('Arial', 12),
width=15, anchor='w').pack(side='left', padx=40)
tk.Checkbutton(frame_row1, text="15 минут", variable=var_15min, font=('Arial', 12),
width=15, anchor='w').pack(side='left', padx=40)
tk.Checkbutton(frame_row1, text="1 час", variable=var_1h, font=('Arial', 12),
width=15, anchor='w').pack(side='left', padx=40)
# Второй ряд чекбоксов (2 таймфрейма)
frame_row2 = tk.Frame(timeframe_frame)
frame_row2.pack(fill='x', pady=15)
tk.Checkbutton(frame_row2, text="4 часа", variable=var_4h, font=('Arial', 12),
width=15, anchor='w').pack(side='left', padx=40)
tk.Checkbutton(frame_row2, text="1 день", variable=var_1d, font=('Arial', 12),
width=15, anchor='w').pack(side='left', padx=40)
# Информация о выбранных тикерах
info_frame = ttk.Frame(main_frame)
info_frame.pack(fill='x', pady=20)
tickers_text = f"Будут обработаны тикеры: {', '.join(selected_tickers)}"
ttk.Label(info_frame, text=tickers_text,
font=('Arial', 11), wraplength=1150).pack(anchor='w')
# Добавляем пояснение о минутных данных с УЧЕТОМ ФОРМАТА ФАЙЛОВ
note_frame = ttk.Frame(main_frame)
note_frame.pack(fill='x', pady=(0, 10))
# Получаем текущий формат файлов из основной вкладки
file_format = self.file_format.get()
extension = file_format.lower()
note_text = f"Примечание: для генерации используются существующие минутные данные (*_1min.{extension})"
ttk.Label(note_frame, text=note_text, font=('Arial', 10),
foreground='gray', wraplength=1150).pack(anchor='w')
# Заполнитель для сдвига кнопок вниз
spacer_frame = ttk.Frame(main_frame)
spacer_frame.pack(fill='both', expand=True)
# Кнопки
button_frame = ttk.Frame(main_frame)
button_frame.pack(fill='x', pady=(0, 10))
ttk.Button(button_frame, text="Сгенерировать",
command=lambda: self.generate_timeframes(selected_tickers, {
# УБИРАЕМ 1min из передаваемых параметров
'5min': var_5min.get(),
'15min': var_15min.get(),
'1h': var_1h.get(),
'4h': var_4h.get(),
'1d': var_1d.get()
}, selection_window),
width=15).pack(side='right', padx=5)
ttk.Button(button_frame, text="Отмена",
command=selection_window.destroy,
width=15).pack(side='right', padx=5)
def generate_timeframes(self, tickers, timeframes, window):
"""Генерирует выбранные таймфреймы для тикеров"""
window.destroy()
self.update_log("=" * 80)
self.update_log("🔄 НАЧАЛО ГЕНЕРАЦИИ ТАЙМФРЕЙМОВ")
self.update_log(f"📋 Тикеры: {', '.join(tickers)}")
selected_timeframes = [tf for tf, selected in timeframes.items() if selected]
self.update_log(f"📊 Таймфреймы: {', '.join(selected_timeframes)}")
save_path = self.update_path_entry.get()
file_format = self.file_format.get()
total_tasks = len(tickers) * len(selected_timeframes)
self.update_progress['value'] = 0
self.update_progress['maximum'] = total_tasks
task_count = 0
successful_generations = 0
for ticker in tickers:
self.update_log(f"🔧 Обработка тикера {ticker}")
# Читаем минутные данные
minute_file = os.path.join(save_path, f"{ticker}_1min.{file_format}")
if not os.path.exists(minute_file):
self.update_log(f"❌ Файл минутных данных не найден: {minute_file}")
continue
try:
minute_data = self.read_existing_file(minute_file, file_format)
if minute_data.empty:
self.update_log(f"❌ Нет данных в файле {minute_file}")
continue
self.update_log(f"📊 Загружено {len(minute_data)} минутных свечей")
# Генерируем выбранные таймфреймы
for timeframe in selected_timeframes:
task_count += 1
self.update_progress['value'] = task_count
# Проверяем, существует ли уже файл
tf_file = os.path.join(save_path, f"{ticker}_{timeframe}.{file_format}")
if os.path.exists(tf_file):
self.update_log(f"ℹ️ Файл уже существует: {ticker}_{timeframe}")
continue
try:
tf_data = self.convert_timeframe(minute_data, timeframe)
if not tf_data.empty:
self.save_to_file(tf_data, tf_file, file_format)
self.update_log(f"💾 Создан: {ticker}_{timeframe} ({len(tf_data)} свечей)")
successful_generations += 1
else:
self.update_log(f"⚠️ Нет данных для {ticker}_{timeframe}")
except Exception as e:
self.update_log(f"❌ Ошибка создания {timeframe} для {ticker}: {str(e)}")
except Exception as e:
self.update_log(f"❌ Ошибка обработки {ticker}: {str(e)}")
self.update_log("=" * 80)
self.update_log(f"✅ ЗАВЕРШЕНО: создано {successful_generations} файлов")
self.update_log("=" * 80)
# Обновляем список файлов
self.root.after(100, self.scan_files)
messagebox.showinfo("Успех", f"Генерация завершена!\nСоздано файлов: {successful_generations}")
def show_ticker_selection(self):
"""Показывает окно выбора тикеров"""
selection_window = tk.Toplevel(self.root)
selection_window.title("Выбор тикеров")
selection_window.geometry("900x700")
selection_window.transient(self.root)
selection_window.grab_set()
# Центрируем окно
selection_window.update_idletasks()
x = (self.root.winfo_screenwidth() - 900) // 2
y = (self.root.winfo_screenheight() - 700) // 2
selection_window.geometry(f"900x700+{x}+{y}")
main_frame = ttk.Frame(selection_window, padding="15")
main_frame.pack(fill='both', expand=True)
# Заголовок и кнопки управления
header_frame = ttk.Frame(main_frame)
header_frame.pack(fill='x', pady=(0, 15))
ttk.Label(header_frame, text="Выберите тикеры для загрузки:", font=('Arial', 12, 'bold')).pack(side='left')
button_frame = ttk.Frame(header_frame)
button_frame.pack(side='right')
ttk.Button(button_frame, text="Выбрать все", command=lambda: self.select_all_tickers(ticker_vars), width=12).pack(side='left', padx=5)
ttk.Button(button_frame, text="Снять все", command=lambda: self.deselect_all_tickers(ticker_vars), width=12).pack(side='left', padx=5)
# Фрейм для поиска
search_frame = ttk.Frame(main_frame)
search_frame.pack(fill='x', pady=(0, 15))
ttk.Label(search_frame, text="Поиск:", font=('Arial', 11)).pack(side='left', padx=(0, 10))
search_var = tk.StringVar()
search_entry = ttk.Entry(search_frame, textvariable=search_var, width=50, font=('Arial', 11))
search_entry.pack(side='left', fill='x', expand=True, padx=(0, 10))
search_entry.bind('<KeyRelease>', lambda e: self.filter_tickers(search_var.get(), ticker_frame, ticker_vars, all_tickers_data))
# Фрейм для добавления тикера вручную
manual_frame = ttk.Frame(main_frame)
manual_frame.pack(fill='x', pady=(0, 15))
ttk.Label(manual_frame, text="Добавить тикер:", font=('Arial', 11)).pack(side='left', padx=(0, 10))
self.manual_ticker_entry = ttk.Entry(manual_frame, width=20, font=('Arial', 11))
self.manual_ticker_entry.pack(side='left', padx=(0, 10))
ttk.Button(manual_frame, text="Добавить", command=lambda: self.add_manual_ticker(ticker_vars, ticker_frame, all_tickers_data), width=10).pack(side='left')
# Фрейм для списка тикеров с прокруткой
list_frame = ttk.Frame(main_frame)
list_frame.pack(fill='both', expand=True)
canvas = tk.Canvas(list_frame, bg='white')
scrollbar = ttk.Scrollbar(list_frame, orient="vertical", command=canvas.yview)
ticker_frame = ttk.Frame(canvas)
ticker_frame.bind(
"<Configure>",
lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
)
canvas.create_window((0, 0), window=ticker_frame, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set)
canvas.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
# Загружаем тикеры с MOEX
ticker_vars = {}
all_tickers_data = {} # Будем хранить данные о всех тикерах
self.load_moex_tickers(ticker_frame, ticker_vars, all_tickers_data)
# Добавляем тикеры из текущего списка, которых нет в MOEX
self.add_custom_tickers(ticker_frame, ticker_vars, all_tickers_data)
# Кнопки внизу
bottom_frame = ttk.Frame(main_frame)
bottom_frame.pack(fill='x', pady=(15, 0))
ttk.Button(bottom_frame, text="Применить",
command=lambda: self.apply_ticker_selection(ticker_vars, selection_window),
width=12).pack(side='right', padx=5)
ttk.Button(bottom_frame, text="Отмена",
command=selection_window.destroy,
width=12).pack(side='right', padx=5)
def add_custom_tickers(self, parent_frame, ticker_vars, all_tickers_data):
"""Добавляет кастомные тикеры из текущего списка"""
current_tickers = [t.strip().upper() for t in self.tickers_entry.get().split(',') if t.strip()]
for ticker in current_tickers:
if ticker not in all_tickers_data:
# Добавляем кастомный тикер
var = tk.BooleanVar(value=True)
ticker_vars[ticker] = var
all_tickers_data[ticker] = {'name': 'добавлен вручную', 'is_custom': True}
# Создаем строку для кастомного тикера
row_frame = ttk.Frame(parent_frame)
row_frame.pack(fill='x', pady=1)
cb = tk.Checkbutton(row_frame, variable=var, bg='white')
cb.pack(side='left', padx=5)
ttk.Label(row_frame, text=ticker, font=('Arial', 10, 'bold'), width=10).pack(side='left', padx=5)
ttk.Label(row_frame, text="(добавлен вручную)", font=('Arial', 10), width=70, anchor='w', foreground='green').pack(side='left', padx=5)
def add_manual_ticker(self, ticker_vars, parent_frame, all_tickers_data):
"""Добавляет тикер вручную"""
ticker = self.manual_ticker_entry.get().strip().upper()
if not ticker:
return
if ticker in ticker_vars:
messagebox.showinfo("Информация", f"Тикер {ticker} уже есть в списке")
return
# Добавляем новый тикер
var = tk.BooleanVar(value=True)
ticker_vars[ticker] = var
all_tickers_data[ticker] = {'name': 'добавлен вручную', 'is_custom': True}
# Создаем строку для нового тикера
row_frame = ttk.Frame(parent_frame)
row_frame.pack(fill='x', pady=1)
cb = tk.Checkbutton(row_frame, variable=var, bg='white')
cb.pack(side='left', padx=5)
ttk.Label(row_frame, text=ticker, font=('Arial', 10, 'bold'), width=10).pack(side='left', padx=5)
ttk.Label(row_frame, text="(добавлен вручную)", font=('Arial', 10), width=70, anchor='w', foreground='green').pack(side='left', padx=5)
self.manual_ticker_entry.delete(0, tk.END)
messagebox.showinfo("Успех", f"Тикер {ticker} добавлен")
def load_moex_tickers(self, parent_frame, ticker_vars, all_tickers_data):
"""Загружает тикеры с MOEX и отображает их"""
try:
url = "https://iss.moex.com/iss/statistics/engines/stock/markets/index/analytics/IMOEX.json?iss.meta=off&limit=9999"
response = requests.get(url, timeout=10)
if response.status_code == 200:
data = response.json()
tickers_data = data['analytics']['data']
# Сортируем по тикеру
tickers_data.sort(key=lambda x: x[2])
# Получаем текущие выбранные тикеры
current_tickers = [t.strip().upper() for t in self.tickers_entry.get().split(',') if t.strip()]
for i, ticker_info in enumerate(tickers_data):
ticker = ticker_info[2] # ticker
name = ticker_info[3] # shortnames
# Сохраняем данные о тикере
all_tickers_data[ticker] = {'name': name, 'is_custom': False}
# Проверяем, выбран ли тикер в текущем списке
var = tk.BooleanVar(value=(ticker in current_tickers))
ticker_vars[ticker] = var
row_frame = ttk.Frame(parent_frame)
row_frame.pack(fill='x', pady=1)
cb = tk.Checkbutton(row_frame, variable=var, bg='white')
cb.pack(side='left', padx=5)
ttk.Label(row_frame, text=ticker, font=('Arial', 10, 'bold'), width=10).pack(side='left', padx=5)
ttk.Label(row_frame, text=name, font=('Arial', 10), width=70, anchor='w').pack(side='left', padx=5)
else:
messagebox.showerror("Ошибка", "Не удалось загрузить тикеры с MOEX")
except Exception as e:
messagebox.showerror("Ошибка", f"Ошибка при загрузке тикеров: {str(e)}")
def filter_tickers(self, search_text, parent_frame, ticker_vars, all_tickers_data):
"""Фильтрует тикеры по поисковому запросу"""
for widget in parent_frame.winfo_children():
widget.destroy()
search_text = search_text.upper()
for ticker, var in ticker_vars.items():
ticker_data = all_tickers_data.get(ticker, {'name': '', 'is_custom': False})
name = ticker_data['name']
is_custom = ticker_data['is_custom']
if search_text in ticker.upper() or search_text in name.upper():
row_frame = ttk.Frame(parent_frame)
row_frame.pack(fill='x', pady=1)
cb = tk.Checkbutton(row_frame, variable=var, bg='white')
cb.pack(side='left', padx=5)
ttk.Label(row_frame, text=ticker, font=('Arial', 10, 'bold'), width=10).pack(side='left', padx=5)
if is_custom:
display_text = "(добавлен вручную)"
color = 'green'
ttk.Label(row_frame, text=display_text, font=('Arial', 10), width=70, anchor='w', foreground=color).pack(side='left', padx=5)
else:
ttk.Label(row_frame, text=name, font=('Arial', 10), width=70, anchor='w').pack(side='left', padx=5)
def select_all_tickers(self, ticker_vars):
"""Выбирает все тикеры"""
for var in ticker_vars.values():
var.set(True)
def deselect_all_tickers(self, ticker_vars):
"""Снимает выделение со всех тикеров"""
for var in ticker_vars.values():
var.set(False)
def apply_ticker_selection(self, ticker_vars, window):
"""Применяет выбранные тикеры"""
selected_tickers = [ticker for ticker, var in ticker_vars.items() if var.get()]
if selected_tickers:
self.tickers_entry.delete(0, tk.END)
self.tickers_entry.insert(0, ", ".join(selected_tickers))
window.destroy()
def browse_folder(self):
"""Выбор папки для сохранения данных"""
path = filedialog.askdirectory()
if path:
self.path_entry.delete(0, tk.END)
self.path_entry.insert(0, path)
def browse_update_folder(self):
"""Выбор папки с данными для обновления"""
path = filedialog.askdirectory()
if path:
self.update_path_entry.delete(0, tk.END)
self.update_path_entry.insert(0, path)
def scan_files(self):
"""Сканирование файлов и отображение списка тикеров"""
# Сохраняем текущее состояние выделения
if hasattr(self, 'ticker_vars') and self.ticker_vars:
self.selected_tickers_state = {}
for ticker, data in self.ticker_vars.items():
self.selected_tickers_state[ticker] = data['var'].get()
# Очищаем предыдущий список
for widget in self.scrollable_frame.winfo_children():
widget.destroy()
self.ticker_vars = {}
save_path = self.update_path_entry.get()
file_format = self.file_format.get()
if not save_path:
messagebox.showerror("Ошибка", "Укажите папку с данными")
return
# Ищем файлы с минутными данными
self.update_log("🔍 Начинаем сканирование файлов...")
pattern = os.path.join(save_path, f"*_1min.{file_format}")
minute_files = glob.glob(pattern)
if not minute_files:
self.update_log("❌ Не найдены файлы с минутными данными")
return
total_files = len(minute_files)
self.update_log(f"📁 Найдено файлов для обработки: {total_files}")
# Прогресс бар для сканирования
self.update_progress['value'] = 0
self.update_progress['maximum'] = total_files
processed_files = 0
for i, file_path in enumerate(minute_files):
# Обновляем прогресс
processed_files += 1
self.update_progress['value'] = processed_files
progress_percent = (processed_files / total_files) * 100
# Извлекаем тикер из имени файла
filename = os.path.basename(file_path)
ticker = filename.split('_')[0].upper()
self.update_log(f"📊 Обрабатываем файл {processed_files}/{total_files} ({progress_percent:.1f}%): {filename}")
# Создаем фрейм для строки
row_frame = ttk.Frame(self.scrollable_frame)
row_frame.pack(fill='x', pady=2)
# Переменная для чекбокса - восстанавливаем состояние если было сохранено
var = tk.BooleanVar(value=self.selected_tickers_state.get(ticker, True))
self.ticker_vars[ticker] = {'var': var, 'file_path': file_path}
# Чекбокс
cb = tk.Checkbutton(row_frame, variable=var, bg='white')
cb.pack(side='left', padx=(20, 10))
# Тикер
ttk.Label(row_frame, text=ticker, font=('Arial', 11), width=15).pack(side='left', padx=10)
# Читаем данные и определяем последнюю дату
try:
existing_data = self.read_existing_file(file_path, file_format)
if not existing_data.empty:
last_date = existing_data.index.max().strftime('%Y-%m-%d %H:%M:%S')
candle_count = len(existing_data)
# Находим все таймфреймы для этого тикера
timeframes = self.find_available_timeframes(ticker, save_path, file_format)
timeframe_text = ", ".join(timeframes) if timeframes else "только 1min"
self.update_log(f" ✅ {ticker}: {candle_count} свечей, последняя дата: {last_date}")
else:
last_date = "Нет данных"
candle_count = 0
timeframe_text = "нет данных"
self.update_log(f" ⚠️ {ticker}: файл пустой или поврежден")
except Exception as e:
last_date = "Ошибка чтения"
candle_count = 0
timeframe_text = f"ошибка: {str(e)}"
self.update_log(f" ❌ {ticker}: ошибка чтения - {str(e)}")
# Последняя дата
ttk.Label(row_frame, text=last_date, font=('Arial', 11), width=20).pack(side='left', padx=10)
# Количество свечей
ttk.Label(row_frame, text=str(candle_count), font=('Arial', 11), width=15).pack(side='left', padx=10)
# Таймфреймы
ttk.Label(row_frame, text=timeframe_text, font=('Arial', 11), width=20).pack(side='left', padx=10)
# Сохраняем информацию о тикере
self.ticker_vars[ticker]['last_date'] = last_date
self.ticker_vars[ticker]['candle_count'] = candle_count
self.ticker_vars[ticker]['timeframes'] = timeframe_text
# Обновляем интерфейс после каждого файла
if i % 5 == 0: # Обновляем каждые 5 файлов для производительности
self.root.update_idletasks()
# Сбрасываем прогресс бар после завершения сканирования
self.update_progress['value'] = 0
self.update_log(f"✅ Сканирование завершено! Обработано файлов: {total_files}")
def find_available_timeframes(self, ticker, save_path, file_format):
"""Находит все доступные таймфреймы для тикера"""
timeframes = []
for tf in ['1min', '5min', '15min', '1h', '4h', '1d']:
file_path = os.path.join(save_path, f"{ticker}_{tf}.{file_format}")
if os.path.exists(file_path):
# Для краткости используем сокращенные обозначения
short_tf = tf.replace('min', 'm').replace('1d', 'D')
timeframes.append(short_tf)
return timeframes
def update_log(self, message):
"""Логирование на вкладке обновления"""
self.update_log_text.insert(tk.END, f"{datetime.now().strftime('%H:%M:%S')} - {message}\n")
self.update_log_text.see(tk.END)
self.root.update_idletasks()
def start_download_thread(self):
"""Запуск загрузки в отдельном потоке"""
if self.downloading:
return
self.downloading = True
self.download_btn.config(state='disabled', bg='gray')
thread = threading.Thread(target=self.download_data)
thread.daemon = True
thread.start()
def start_update_thread(self):
"""Запуск обновления данных в отдельном потоке"""
if self.downloading:
return
self.downloading = True
self.update_btn.config(state='disabled', bg='gray')
thread = threading.Thread(target=self.update_selected_data)
thread.daemon = True
thread.start()
def log(self, message):
"""Метод для логирования в текстовое поле"""
self.log_text.insert(tk.END, f"{datetime.now().strftime('%H:%M:%S')} - {message}\n")
self.log_text.see(tk.END)
self.root.update_idletasks()
def update_status(self, message):
"""Обновление статусной строки"""
self.status_var.set(message)
self.root.update_idletasks()
def is_weekend(self, date):
"""Проверка выходного дня"""
return date.weekday() >= 5
def download_data(self):
"""Полная загрузка данных"""
try:
self.update_status("Начинаем загрузку данных...")
tickers = [t.strip().upper() for t in self.tickers_entry.get().split(',')]
start_date = self.start_entry.get()
end_date = self.end_entry.get()
save_path = self.path_entry.get()
file_format = self.file_format.get()
only_weekdays = self.only_weekdays.get()
if not all([tickers, start_date, end_date, save_path]):
messagebox.showerror("Ошибка", "Все поля должны быть заполнены")
self.update_status("Ошибка: не все поля заполнены")
return
# Получаем выбранные таймфреймы
timeframes = self.get_selected_timeframes()
if not timeframes:
messagebox.showerror("Ошибка", "Выберите хотя бы один таймфрейм")
self.update_status("Ошибка: не выбран таймфрейм")
return
os.makedirs(save_path, exist_ok=True)
total_tasks = len(tickers) * len(timeframes)
self.progress['value'] = 0
self.progress['maximum'] = total_tasks
task_count = 0
successful_downloads = 0
self.log("=" * 80)
self.log(f"🚀 НАЧАЛО ПОЛНОЙ ЗАГРУЗКИ ДАННЫХ")
self.log(f"Тикеры: {', '.join(tickers)}")
self.log(f"Период: {start_date} - {end_date}")
self.log(f"Таймфреймы: {', '.join(timeframes)}")
self.log(f"Формат файлов: {file_format.upper()}")
self.log(f"Только будни: {'ДА' if only_weekdays else 'НЕТ'}")
self.log(f"Папка сохранения: {save_path}")
self.log("=" * 80)
for ticker in tickers:
if not self.downloading:
break
self.current_ticker = ticker
self.update_status(f"Обработка тикера {ticker}...")
self.log(f"📥 Начинаем загрузку {ticker}")
try:
# Загружаем 1-минутные данные (основные)
minute_data = self.download_1min_data(ticker, start_date, end_date, only_weekdays)
if minute_data.empty:
self.log(f"❌ Нет данных для {ticker}")
continue
self.log(f"✅ Загружено {len(minute_data)} минутных свечей для {ticker}")
# Генерируем все таймфреймы из 1-минутных данных
for timeframe in timeframes:
if not self.downloading:
break
task_count += 1
self.progress['value'] = task_count
self.update_status(f"Генерация {timeframe} для {ticker}...")
self.log(f"Создание {timeframe} для {ticker}")
try:
if timeframe == '1min':
tf_data = self.prepare_1min_data(minute_data)
else:
tf_data = self.convert_timeframe(minute_data, timeframe)
if not tf_data.empty:
extension = file_format.lower()
filename = os.path.join(save_path, f"{ticker}_{timeframe}.{extension}")
self.save_to_file(tf_data, filename, file_format)
self.log(f"💾 Сохранено: {filename} ({len(tf_data)} свечей)")
successful_downloads += 1
else:
self.log(f"⚠️ Нет данных для {ticker}_{timeframe}")
except Exception as e:
self.log(f"❌ Ошибка создания {timeframe} для {ticker}: {str(e)}")
time.sleep(0.2)
except Exception as e:
self.log(f"❌ Ошибка для {ticker}: {str(e)}")
self.downloading = False
self.download_btn.config(state='normal', bg='#0078D7')
self.update_status("Загрузка завершена")
# СБРАСЫВАЕМ ПРОГРЕСС-БАР В 0
self.progress['value'] = 0
self.day_progress['value'] = 0
self.log("=" * 80)
self.log(f"✅ ЗАВЕРШЕНО: Создано {successful_downloads} файлов")
self.log("=" * 80)
messagebox.showinfo("Успех", f"Данные успешно скачаны!\nСоздано файлов: {successful_downloads}")
except Exception as e:
self.downloading = False
self.download_btn.config(state='normal', bg='#0078D7')
self.update_status("Ошибка при загрузке")
# СБРАСЫВАЕМ ПРОГРЕСС-БАР В 0 ПРИ ОШИБКЕ
self.progress['value'] = 0
self.day_progress['value'] = 0
messagebox.showerror("Ошибка", str(e))
def update_selected_data(self):
"""Обновление выбранных тикеров"""
try:
self.update_log("=" * 80)
self.update_log("🔄 НАЧАЛО ОБНОВЛЕНИЯ ВЫБРАННЫХ ТИКЕРОВ")
# Получаем выбранные тикеры
selected_tickers = []
for ticker, data in self.ticker_vars.items():
if data['var'].get():
selected_tickers.append(ticker)
if not selected_tickers:
self.update_log("❌ Не выбрано ни одного тикера для обновления")
return
self.update_log(f"📋 Выбрано тикеров: {len(selected_tickers)}")
self.update_log(f"📊 Тикеры: {', '.join(selected_tickers)}")
# Получаем конечную дату обновления
end_date = self.update_end_date.get()
if not end_date:
end_date = datetime.now().strftime('%Y-%m-%d')
self.update_log(f"📅 Дата обновления не указана, используем текущую: {end_date}")
else:
self.update_log(f"📅 Обновляем до даты: {end_date}")
only_weekdays = self.only_weekdays.get()
file_format = self.file_format.get()
save_path = self.update_path_entry.get()
total_tasks = len(selected_tickers)
self.update_progress['value'] = 0
self.update_progress['maximum'] = total_tasks
task_count = 0
successful_updates = 0
for ticker in selected_tickers:
if not self.downloading:
break
file_path = self.ticker_vars[ticker]['file_path']
self.update_log(f"🔄 Обработка тикера {ticker}")
try:
# Читаем существующие данные
existing_data = self.read_existing_file(file_path, file_format)
if existing_data.empty:
self.update_log(f"⚠️ Файл {ticker} пустой или поврежден")
continue
# Определяем последнюю дату в файле
last_date = existing_data.index.max().date()
start_date = (last_date + timedelta(days=1)).strftime('%Y-%m-%d')
self.update_log(f"📊 В файле: {len(existing_data)} свечей, последняя дата: {last_date}")
self.update_log(f"📥 Будет загружено с {start_date} по {end_date}")
# Загружаем новые данные
new_data = self.download_1min_data(ticker, start_date, end_date, only_weekdays)
if new_data.empty:
self.update_log(f"ℹ️ Нет новых данных для {ticker}")
successful_updates += 1
continue
self.update_log(f"✅ Загружено {len(new_data)} новых свечей")
# Объединяем данные
combined_data = pd.concat([existing_data, new_data], ignore_index=False)
combined_data = combined_data[~combined_data.index.duplicated(keep='last')]
combined_data = combined_data.sort_index()
# Сохраняем обновленный минутный файл
task_count += 1
self.update_progress['value'] = task_count
save_data = self.prepare_1min_data(combined_data)
self.save_to_file(save_data, file_path, file_format)
self.update_log(f"💾 Обновлен: {ticker} ({len(combined_data)} свечей)")
# Обновляем существующие таймфреймы
self.update_existing_timeframes(ticker, save_path, file_format, combined_data)
successful_updates += 1
except Exception as e:
self.update_log(f"❌ Ошибка обновления {ticker}: {str(e)}")
self.downloading = False
self.update_btn.config(state='normal', bg='#28A745')
# СБРАСЫВАЕМ ПРОГРЕСС-БАР В 0 ПОСЛЕ ОБНОВЛЕНИЯ
self.update_progress['value'] = 0
# Автоматическое обновление списка после завершения
self.root.after(100, self.scan_files)
self.update_log("=" * 80)
self.update_log(f"✅ ЗАВЕРШЕНО ОБНОВЛЕНИЕ: обработано {successful_updates} тикеров")
self.update_log("=" * 80)
messagebox.showinfo("Успех", f"Данные успешно обновлены!\nОбработано тикеров: {successful_updates}")
except Exception as e:
self.downloading = False
self.update_btn.config(state='normal', bg='#28A745')
# СБРАСЫВАЕМ ПРОГРЕСС-БАР В 0 ПРИ ОШИБКЕ
self.update_progress['value'] = 0
self.update_log(f"❌ Ошибка при обновлении: {str(e)}")
messagebox.showerror("Ошибка", str(e))
def update_existing_timeframes(self, ticker, save_path, file_format, minute_data):
"""Обновление существующих таймфреймов для тикера"""
# Ищем все существующие файлы для этого тикера
pattern = os.path.join(save_path, f"{ticker}_*.{file_format}")
existing_files = glob.glob(pattern)
for file_path in existing_files:
filename = os.path.basename(file_path)
# Пропускаем минутный файл, он уже обновлен
if filename.endswith(f"_1min.{file_format}"):
continue
# Извлекаем таймфрейм из имени файла
try:
timeframe = filename.replace(f"{ticker}_", "").replace(f".{file_format}", "")
if timeframe in ['5min', '15min', '1h', '4h', '1d']:
self.update_log(f"🔄 Обновление {timeframe} для {ticker}")
try:
tf_data = self.convert_timeframe(minute_data, timeframe)
if not tf_data.empty:
self.save_to_file(tf_data, file_path, file_format)
self.update_log(f"💾 Обновлен: {ticker}_{timeframe} ({len(tf_data)} свечей)")
else:
self.update_log(f"⚠️ Нет данных для {ticker}_{timeframe}")
except Exception as e:
self.update_log(f"❌ Ошибка обновления {timeframe} для {ticker}: {str(e)}")
except Exception as e:
self.update_log(f"❌ Ошибка обработки файла {filename}: {str(e)}")
def get_selected_timeframes(self):
"""Получение списка выбранных таймфреймов"""
timeframes = []
if self.var_1min.get(): timeframes.append('1min')
if self.var_5min.get(): timeframes.append('5min')
if self.var_15min.get(): timeframes.append('15min')
if self.var_1h.get(): timeframes.append('1h')
if self.var_4h.get(): timeframes.append('4h')
if self.var_1d.get(): timeframes.append('1d')
return timeframes
def download_1min_data(self, ticker, start_date, end_date, only_weekdays=True):
"""Загрузка 1-минутных данных с MOEX API"""
all_data = []
current_start = datetime.strptime(start_date, '%Y-%m-%d')
end = datetime.strptime(end_date, '%Y-%m-%d')
total_days = 0
temp_date = current_start
while temp_date <= end:
if not only_weekdays or not self.is_weekend(temp_date):
total_days += 1
temp_date += timedelta(days=1)
self.day_progress['value'] = 0
self.day_progress['maximum'] = total_days
self.log(f"📥 Загрузка 1-минутных данных для {ticker}")
self.log(f"Период: {start_date} - {end_date}")
self.log(f"Всего дней для загрузки: {total_days} {'(только будни)' if only_weekdays else ''}")
days_loaded = 0
while current_start <= end and self.downloading:
if only_weekdays and self.is_weekend(current_start):
current_start += timedelta(days=1)
continue
try:
date_str = current_start.strftime('%Y-%m-%d')
days_loaded += 1
self.day_progress['value'] = days_loaded
self.update_status(f"{ticker}: день {days_loaded}/{total_days} (загрузка...)")
# Загружаем данные дня частями (из-за ограничения в 500 свечей)
day_data = []
from_time = "00:00:00" # ВОССТАНОВЛЕНО правильное время!
day_requests = 0
while from_time and self.downloading:
day_requests += 1
url = f"http://iss.moex.com/iss/engines/stock/markets/shares/securities/{ticker}/candles.json"
params = {
'from': f"{date_str} {from_time}",
'till': f"{date_str} 23:59:59",
'interval': 1,
'iss.meta': 'off'
}
response = requests.get(url, params=params, timeout=10)
if response.status_code == 200:
data = response.json()
candles = data['candles']['data']
if candles:
columns = ['open', 'close', 'high', 'low', 'value', 'volume', 'begin', 'end']
df = pd.DataFrame(candles, columns=columns)
df['begin'] = pd.to_datetime(df['begin'])
df.set_index('begin', inplace=True)
df = df[['open', 'high', 'low', 'close', 'volume']]
day_data.append(df)
if len(candles) < 500:
break
last_time = df.index.max() + timedelta(minutes=1)
from_time = last_time.strftime('%H:%M:%S')
self.log(f" ⏱️ Загружено {len(candles)} свечей, продолжаем с {from_time}")
time.sleep(0.1)
else:
break
else:
self.log(f" 📅 {date_str}: ошибка HTTP {response.status_code}")
break
if day_data:
day_df = pd.concat(day_data, ignore_index=False)
all_data.append(day_df)
total_candles = len(day_df)
first_time = day_df.index.min().strftime('%H:%M:%S')
last_time = day_df.index.max().strftime('%H:%M:%S')
self.log(f" 📅 {date_str}: {total_candles} свечей за {day_requests} запр., время: {first_time} - {last_time}")
else:
self.log(f" 📅 {date_str}: нет данных")
except requests.exceptions.Timeout:
self.log(f" 📅 {date_str}: таймаут")
except Exception as e:
self.log(f" 📅 {date_str}: ошибка {str(e)}")
current_start += timedelta(days=1)
time.sleep(0.05)
# Объединяем все данные и сортируем по времени
if all_data:
result = pd.concat(all_data, ignore_index=False)
result = result.sort_index()
if isinstance(result, pd.Series):
result = result.to_frame().T
self.log(f"✅ {ticker}: загружено {len(result)} свечей за {days_loaded} дней")
self.update_status(f"{ticker}: завершено")
return result
else:
self.log(f"❌ {ticker}: не удалось загрузить данные")
return pd.DataFrame()
def prepare_1min_data(self, df):
"""Подготовка 1-минутных данных к сохранению"""
if df.empty:
return df
result = df.copy()
# Если данные уже имеют колонки DATE и TIME, используем их
if 'DATE' not in result.columns or 'TIME' not in result.columns:
# Сбрасываем индекс чтобы получить колонку с временем
result = result.reset_index()
# Переименовываем временную колонку если нужно
if 'begin' in result.columns:
result = result.rename(columns={'begin': 'timestamp'})
elif 'index' in result.columns:
result = result.rename(columns={'index': 'timestamp'})
# Создаем колонки DATE и TIME из временной метки
if 'timestamp' in result.columns:
result['DATE'] = result['timestamp'].dt.strftime('%Y%m%d')
result['TIME'] = result['timestamp'].dt.strftime('%H%M%S')
# Убираем временную колонку
result = result.drop('timestamp', axis=1)
# Переименовываем колонки в нужный формат (всегда в верхний регистр без скобок)
column_mapping = {}
if 'open' in result.columns:
column_mapping['open'] = 'OPEN'
if 'high' in result.columns:
column_mapping['high'] = 'HIGH'
if 'low' in result.columns:
column_mapping['low'] = 'LOW'
if 'close' in result.columns:
column_mapping['close'] = 'CLOSE'
if 'volume' in result.columns:
column_mapping['volume'] = 'VOL'
if column_mapping:
result = result.rename(columns=column_mapping)
# Убедимся, что все нужные колонки присутствуют
required_columns = ['DATE', 'TIME', 'OPEN', 'HIGH', 'LOW', 'CLOSE', 'VOL']
available_columns = [col for col in required_columns if col in result.columns]
# Если каких-то колонок нет, логируем это
missing_columns = set(required_columns) - set(available_columns)
if missing_columns:
self.log(f" ⚠️ Отсутствуют колонки: {missing_columns}")
return result[available_columns]
def convert_timeframe(self, df, timeframe):
"""Конвертация в другие таймфреймов - ОКОНЧАТЕЛЬНАЯ ИСПРАВЛЕННАЯ ВЕРСИЯ"""
if df.empty:
return df
df_copy = df.copy()
# Логируем структуру данных для отладки
self.log(f" 🔍 Структура данных для конвертации {timeframe}:")
self.log(f" Колонки: {list(df_copy.columns)}")
self.log(f" Тип индекса: {type(df_copy.index)}")
if hasattr(df_copy.index, 'name'):
self.log(f" Имя индекса: {df_copy.index.name}")
# Определяем правило ресемплинга
if timeframe == '5min':
rule = '5T'
elif timeframe == '15min':
rule = '15T'
elif timeframe == '1h':
rule = '1H'
elif timeframe == '4h':
rule = '4H'
elif timeframe == '1d':
rule = '1D'
else:
return df
# Определяем какие колонки агрегировать
agg_dict = {}
if 'open' in df_copy.columns:
agg_dict['open'] = 'first'
elif 'OPEN' in df_copy.columns:
agg_dict['OPEN'] = 'first'
if 'high' in df_copy.columns:
agg_dict['high'] = 'max'
elif 'HIGH' in df_copy.columns:
agg_dict['HIGH'] = 'max'
if 'low' in df_copy.columns:
agg_dict['low'] = 'min'
elif 'LOW' in df_copy.columns:
agg_dict['LOW'] = 'min'
if 'close' in df_copy.columns:
agg_dict['close'] = 'last'
elif 'CLOSE' in df_copy.columns:
agg_dict['CLOSE'] = 'last'
if 'volume' in df_copy.columns:
agg_dict['volume'] = 'sum'
elif 'VOL' in df_copy.columns:
agg_dict['VOL'] = 'sum'
if not agg_dict:
self.log(" ❌ Нет данных для агрегации")
return pd.DataFrame()
self.log(f" 🔧 Агрегация колонок: {list(agg_dict.keys())}")
try:
# Выполняем ресемплинг
result = df_copy.resample(rule).agg(agg_dict).dropna()
if result.empty:
self.log(f" ⚠️ После ресемплинга нет данных")
return pd.DataFrame()
self.log(f" ✅ Ресемплинг выполнен: {len(result)} свечей")
# Сбрасываем индекс чтобы добавить DATE и TIME
result = result.reset_index()
# Переименовываем временную колонку (может называться 'begin' или 'index')
if 'begin' in result.columns:
timestamp_col = 'begin'
elif 'index' in result.columns:
timestamp_col = 'index'
else:
# Если не нашли стандартные имена, берем первую колонку (это будет временная метка)
timestamp_col = result.columns[0]
# Добавляем колонки DATE и TIME
if timeframe == '1d':
result['DATE'] = result[timestamp_col].dt.strftime('%Y%m%d')
result['TIME'] = '000000'
else:
result['DATE'] = result[timestamp_col].dt.strftime('%Y%m%d')
result['TIME'] = result[timestamp_col].dt.strftime('%H%M%S')
# Убираем временную колонку
result = result.drop(timestamp_col, axis=1)
# Переименовываем колонки в верхний регистр
result = result.rename(columns={
'open': 'OPEN', 'high': 'HIGH', 'low': 'LOW',
'close': 'CLOSE', 'volume': 'VOL'
})
# Убедимся, что все нужные колонки присутствуют
required_columns = ['DATE', 'TIME', 'OPEN', 'HIGH', 'LOW', 'CLOSE', 'VOL']
available_columns = [col for col in required_columns if col in result.columns]
self.log(f" 💾 Подготовлено данных: {len(result)} строк, колонки: {available_columns}")
return result[available_columns]
except Exception as e:
self.log(f" ❌ Ошибка ресемплинга для {timeframe}: {str(e)}")
import traceback
self.log(f" 📋 Детали ошибки: {traceback.format_exc()}")
return pd.DataFrame()
def read_existing_file(self, file_path, file_format):
"""Чтение существующего файла с данными"""
try:
if file_format == 'csv':
# CSV формат: <DATE>, <TIME>, <OPEN>, etc.
df = pd.read_csv(file_path)
self.log(f" Прочитано {len(df)} строк (CSV формат)")
# Проверяем колонки CSV формата
if '<DATE>' not in df.columns or '<TIME>' not in df.columns:
self.log(f"❌ CSV файл не содержит колонок <DATE> и <TIME>")
self.log(f" Найденные колонки: {list(df.columns)}")
return pd.DataFrame()
# Создаем временную метку из <DATE> и <TIME>
try:
# Преобразуем числовые колонки в строки с заполнением нулями
date_str = df['<DATE>'].astype(str).str.zfill(8)
time_str = df['<TIME>'].astype(str).str.zfill(6)
# Создаем временную метку
df['timestamp'] = pd.to_datetime(date_str + ' ' + time_str, format='%Y%m%d %H%M%S')
# Устанавливаем временную метку как индекс
df.set_index('timestamp', inplace=True)
# Переименовываем колонки в стандартные имена
column_mapping = {
'<OPEN>': 'open',
'<HIGH>': 'high',
'<LOW>': 'low',
'<CLOSE>': 'close',
'<VOL>': 'volume'
}
df = df.rename(columns=column_mapping)
# Оставляем только нужные колонки
df = df[['open', 'high', 'low', 'close', 'volume']]
self.log(f"✅ Успешно созданы временные метки из <DATE> и <TIME>")
self.log(f" Диапазон данных: {df.index.min()} - {df.index.max()}")
return df
except Exception as e:
self.log(f"❌ Ошибка создания временных меток: {e}")
return pd.DataFrame()
else: # txt format
# TXT формат: DATE, TIME, OPEN, etc. (без скобок)
df = pd.read_csv(file_path, sep='\t')
self.log(f" Прочитано {len(df)} строк (TXT формат)")
# Проверяем колонки TXT формата
if 'DATE' not in df.columns or 'TIME' not in df.columns:
self.log(f"❌ TXT файл не содержит колонок DATE и TIME")
self.log(f" Найденные колонки: {list(df.columns)}")
return pd.DataFrame()
# Создаем временную метку из DATE и TIME
try:
# Преобразуем числовые колонки в строки с заполнением нулями
date_str = df['DATE'].astype(str).str.zfill(8)
time_str = df['TIME'].astype(str).str.zfill(6)
# Создаем временную метку
df['timestamp'] = pd.to_datetime(date_str + ' ' + time_str, format='%Y%m%d %H%M%S')
# Устанавливаем временную метку как индекс
df.set_index('timestamp', inplace=True)
# Оставляем только нужные колонки
df = df[['OPEN', 'HIGH', 'LOW', 'CLOSE', 'VOL']]
# Переименовываем в нижний регистр для единообразия
df = df.rename(columns={
'OPEN': 'open',
'HIGH': 'high',
'LOW': 'low',
'CLOSE': 'close',
'VOL': 'volume'
})
self.log(f"✅ Успешно созданы временные метки из DATE и TIME")
self.log(f" Диапазон данных: {df.index.min()} - {df.index.max()}")
return df
except Exception as e:
self.log(f"❌ Ошибка создания временных меток: {e}")
return pd.DataFrame()
except Exception as e:
self.log(f"❌ Ошибка чтения файла {file_path}: {str(e)}")
return pd.DataFrame()
def save_to_file(self, df, filename, file_format):
"""Сохранение данных в файл"""
if df.empty:
self.log(f"⚠️ Пустые данные для {filename}")
return
try:
with open(filename, 'w', encoding='utf-8') as f:
if file_format == 'csv':
# CSV формат с угловыми скобками
f.write("<DATE>,<TIME>,<OPEN>,<HIGH>,<LOW>,<CLOSE>,<VOL>\n")
for _, row in df.iterrows():
line = (f"{row['DATE']},{row['TIME']},{row['OPEN']:.7f},{row['HIGH']:.7f},"
f"{row['LOW']:.7f},{row['CLOSE']:.7f},{row['VOL']:.0f}\n")
f.write(line)
else:
# TXT формат без скобок
f.write("DATE\tTIME\tOPEN\tHIGH\tLOW\tCLOSE\tVOL\n")
for _, row in df.iterrows():
line = (f"{row['DATE']}\t{row['TIME']}\t{row['OPEN']:.7f}\t{row['HIGH']:.7f}\t"
f"{row['LOW']:.7f}\t{row['CLOSE']:.7f}\t{row['VOL']:.0f}\n")
f.write(line)
self.log(f"💾 Успешно сохранено: {filename} ({len(df)} строк)")
except Exception as e:
self.log(f"❌ Ошибка сохранения {filename}: {str(e)}")
if __name__ == "__main__":
root = tk.Tk()
app = MoexDataDownloader(root)
root.mainloop()
Версия 1.1
Появился 30м таймфрейм.
Окно «Обновление данных» значительно шустрее.
LUA я вообще никогда в глаза не видел, разве что когда игры какие-то в детстве ковырял со скриптами на ней :)
А задача — да, именно такая была — иметь котировки и способ их обновления. Ну и вторичная — мне как идея понравился эксперимент написать/нагенерировать с помощью ИИ что-то на питоне с графическим UI, раньше такого не делал.
Пример вызова:
Вот так: iss.moex.com/iss/engines/stock/markets/shares/securities/GAZP/candles.json?from=2024-01-15%2009:30:00&till=2024-01-15%2018:45:00&interval=1&iss.meta=off
и переносы торговых дней, например суббота рабочая — понедельник нерабочий ?
Никакой логики не вводил для этого. Берет тупо информацию о ценах дневных свечей через API мосбиржи.
Тоже никак сейчас, там стоит условие номер дня > 5 значит выходной.
Тут надо глянуть предоставляет ли API биржи информацию о подобных днях, а если нет — то использовать рабочий календарь. Возможно и предоставляет, я не смотрел.
Опция качать только данные по будням (пн-пт) в наличии, но это все что есть пока.
Но он же все в текстовый файл складывает? Т.е надо ручками подправить для своих нужд, если что.
Ну или сделать отдельную вкладку прямо в этом. Но я к тому, что это отдельно выделенный логический функционал должен быть, мне кажется. С отдельными названиями файлов свечей обработанных, в частности.
Конкретно для ВТБ, я бы еще их IPO бесконечные учитывал
тогда и free float надо привязывать.
Может быть если лоты добавить в начальные данные, то можно упростить будет. Т.е вначале будут данные у ВТБ 10000 акций и цена 0,02, а потом перейдет в 1 акция и 70 рублей. Так вроде всё более гладко должно быть
Вообще, как я понял, под виндой запустилась у одного человека (уточняю). Может там и нет проблем никаких. Пробовать надо.
Upd: работа под виндой тоже подтверждена одним человеком.
XXX★,
А как запускать под виндой, через интерпретатор Питона или надо загрузить фреймворк какой-то?
www.kaggle.com/datasets/olegshpagin/russia-stocks-prices-ohlcv
у чела они постоянно качаются и обновляются
Автор тыж погроммист, вроде, был, когда-то, выкладывать код постом — людей не уважать!
Для кода есть github или посконный gitflic.ru/
Для тех, кто не работает на питоне наверное следует подсказать, что кроме установки питона еще надо установить библиотеки pandas и requests.
А почему бы не сделать универсальную выгрузку — не только для акций, но и фьючерсов? Было бы совсем здорово. У меня на lua так реализовано, но конечно не так красиво — без оконного интерфейса, lua с ним не дружит.
Все работает, только Касперский достал. Никак не доходят руки настроить его по уму.
Йоганн, NumPy вроде в последней стабильной версии питона автоматом есть. Хотя согласен с вашим дополнением. Чтобы не гадать есть она или нет, можно проверить так:
import numpy as np
print(np.__version__)
напечатается версия numpy, а если не установлена — выдаст ошибку. Я не на питоне пишу, у меня она уже была раньше.
Докладываю:
Установил на 10-ю винду Python 3.14 вместе с PIP
Установил pandas и requests.
Запустил — работает. Но почти все поле приложения находится за пределами экрана. Разрешение моего экрана 1366х768.
Изменил код:
self.root.title(«MOEX Data Downloader — Скачивание данных Московской биржи»)
self.root.geometry(«1366x768»)
# Центрируем окно на экране
self.root.update_idletasks()
x = (self.root.winfo_screenwidth() — 1366) // 2
y = (self.root.winfo_screenheight() — 768) // 2
self.root.geometry(f«1366x768+{x}+{y}»)
=========================
Может, имеет смысл сделать версию с адаптивным разрешением?
Я так понял, немного изучив вопрос, что это может быть несколько трудоемко. Я пока не готов этим заниматься. У меня в приоритете новый функционал пока что. Ссорян.
И не было бы проблем с резайзом UI.
Ужос
IDE для питона использую Spyder, все само поставило, вставил только скрипт и запустил.
Spyder качал с офф сайта: www.spyder-ide.org/
Если не пашет есть копия на гитхабе: github.com/spyder-ide/spyder
Рекомендую 2.0 тк она прям значительно быстрее. А в следующих верисиях будет уже новый другой функционал, помимо загрузок котировок. М.б многим хватит и 2.0