Наконец-то я закончил работу над своей первой настоящей, правда еще консольной, программой, с помощью которой можно скачать все исторические данные (свечки OHLCV) с различными таймфреймами по всем акциям Мосбиржи. Рассчитывал сделать все за 2-3 дня, а по факту работа затянулась дней на 10. Однако и в этом есть огромный плюс – кажется я все больше начинаю понимать как программировать, хотя осознаю, что знаний в безграничном python катастрофически не хватает. И похоже начинается реальный кодинг, а этот процесс пожалуй единственный способ чему-то научиться. Открыл для себя несколько новых библиотек, изучил информацию о 3 инсталляторах. Получилось сделать то, о чем не мог себе представить еще месяц назад. Открывая сейчас код программы начинаю чувствую на подсознании, что не все так страшно, как было совсем недавно, когда подходил к монитору сына – школьника и смотрел на его листинг с полным непониманием и внутренним “что это?”.
Итак, в конце года я писал о том, как с помощью Algopack можно вытащить справочную информацию о всех акциях Мосбиржи. Был написан мой первый небольшой и достаточно простой скрипт использующий библиотеку moexalgo. И я обозначил планы дописать его с целью добычи всех исторических данных.
Тахометр трейдера
Сказано – сделано. В итоге получилась, как я считаю, вполне полноценная программа. Естественно захотелось скомпилировать программу под windows. Дальше больше – придумал ей название “Тахометр Трейдера”)) (Был у меня в давней истории небольшой период, когда продавал тахометры для подвесных лодочных моторов и был даже сделанный за деньги небольшой интернет магазин “Тахометр” со слоганом “живи ритмично”. Так что название программы пришло как-то само и сразу.)
Ну а теперь к делу. Для программы я сверстал на генераторе статичных сайтов MkDocs (тоже на python) сайт с пользовательским мануалом – https://tahometr.ru/. С него же можно скачать дистрибутив программы. Качайте, буду очень рад, если программа сможет пригодиться Вам.
Статистика по добываемой программой данным на сегодняшний день такая:
С таймфреймом D (день) по всем 249 акциям сохранено 481118 строк, объем файлов составил 38.5 Мб.
С таймфреймом 60 мин по всем 249 акциям сохранено 3809927 строк, объем файлов составил 306.4 Мб.
С таймфреймом 10 мин по всем 249 акциям сохранено 16050063 строк, объем файлов составил 1285.3 Мб.
С таймфреймом 1 мин по всем 249 акциям сохранено 94300353 строк, объем файлов составил 7520.3 Мб.
Вообще всего в 996 файлах исторических данных сохранено 114,6 миллиона строк(свечек), общий объем сохраненных данных 9.15 Гб.
Для этой статистики дописал отдельный скрипт, который можно просто положить рядом с файлом программы и запускать, когда появится интерес.
Программа скомпилирована инсталлятором PyInstaller.
ALGOPACK
Программа использует официальную библиотеку moexalgo для работы с Algopack. Что интересного выяснилось?
- Негативный момент. Уже после сборки программы во время тестирования Алгопак при цикличном обращении к нему стал неожиданно отдавать ошибку, стал разбираться – ошибка касается акции “VEON-RX”. Представитель мосбиржи в телеграмме обещал “косяк” этот поправить. Но, к сожалению, спустя 2 недели ситуация не изменилась. Пришлось сделать обработчик ошибок. Мало ли по какой еще акции будут подобные фичи.
- Приятный момент. На официальной странице ЧТО ТАКОЕ ALGOPACK? отмечено, что по Super Candles история с 2020 года. Однако как выяснилось по факту такого ограничения нет, даты начала данных рознятся по всем акциям и начинаются с 2011 года. Например по Сберу минутки доступны с 2011-12-15 10:00:00 и сейчас скачивается 1,8 млн записей в объеме 160мБ
- У Алгопак функционал намного шире, я использовал только один метод Ticker.candles(). В планах изучить остальные возможности и при целесообразности добавить их к функционалу программы.
Документация программы
Программа проста в использовании. Тем не менее я написал небольшой мануал и снял отдельное видео.
Листинг программы
Детально описывать весь алгоритм программы не сложно, но на самом деле уже достаточно трудоемко и долго. Возможно в след раз как минимум буду рисовать блок схему. Листинг прикрепляю ниже. Если будут вопросы по коду – обязательно отвечу.
import csv
import os.path
import re
import sys
import time as t_time
import webbrowser
from datetime import date, datetime, time, timedelta
import pandas as pd
from tqdm import tqdm
import moexalgo as moex
from moexalgo import Market, Ticker
# ------------ФУНКЦИИ------------------------
def console_title(title):
"""Функция отображения названия окна консоли"""
if os.name == "nt":
# Для Windows используем команду 'title'
os.system("title " + title)
else:
# Для других операционных систем используем команду 'echo'
os.system("echo -n -e '\033]0;" + title + "\a' > /dev/tty")
def log_plus(text):
"""Функция для записи в логфайл и вывода в консоль"""
with open(filelog_path, "a", encoding="utf-8") as logfile:
# Получаем текущее время
current_time = datetime.now().replace(microsecond=0)
# Форматируем строку для записи в лог
log_entry = f"{current_time}: {text}\n"
# # Записываем строку в файл
logfile.write(log_entry)
print(log_entry, end="")
def secid_candles():
"""Функция для скачивания исторических данных по акциям"""
# ===Главный цикл последовательного сбора данных по каждой акции из списка===
for SECID, FIRST_DATE in tuple_list:
svech = 0
# date = datetime.strptime(FIRST_DATE, "%Y-%m-%d %H:%M:%S").replace(
# hour=0, minute=0, second=0
# )
# дата конца диапазона выдачи данных - текущее время, в каждой акции обновляем чтобы максимально свежее данные были, особенно минутки
till_date = datetime.now().replace(microsecond=0)
log_plus(f"Занимаемся акцией - '{SECID}'")
# проверяем есть ли файл с котировками для данной акции, если нет - создаем
filestocks_path = os.path.join(folder_path, f"{SECID}-{period}.txt")
if os.path.exists(filestocks_path):
log_plus(
f"Файл '{filestocks_path}' для хранениния котировок акций '{SECID}' с таимфреймом {period} уже существует"
)
# читаем файл с котировками, смотрим не пустой ли, если нет - вытаскиваем последнюю сохраненную дату
with open(filestocks_path, "r", encoding="utf-8") as file:
reader = csv.reader(file)
row_count = sum(1 for row in reader) # считаем количество строк
if row_count > 1:
# Перемещаем указатель файла в начало
file.seek(
0
) # перемещаем указатель в начало иначе след строка вернет пустое значение
last_row = file.readlines()[-1] # считываем последнюю строку
last_time = last_row.split("\t")[
0
] # берем первый элемент строки - дату
date = datetime.strptime(
last_time, "%Y-%m-%d %H:%M:%S"
) # преобразуем str в datetime
log_plus(
f"Последние сохраненные данные от {date}, в файле имеется {row_count} строк"
)
else:
log_plus(f"файл '{filestocks_path}' пустой")
else:
log_plus(
f"Файл {filestocks_path} для хранениния котировок акций '{SECID}' с таимфреймом {period} отсутствует и будет создан после сбора данных"
)
# создаем пустый файл для котировок с заголовками в первой строке
df_load = pd.DataFrame(
columns=[
"begin",
"end",
"open",
"high",
"low",
"close",
"value",
"volume",
]
)
df_load.to_csv(
filestocks_path, mode="w", sep="\t", index=False, header=True
)
# берем за начальную дату FIRST_DATE из tuple_list на основе list_tools_listlevel.txt, т.к. ранее скачанных свечей не было
# с проверкой не является ли FIRST_DATE пустым - последствие обработки ошибки Мосбиржи, в частности для VEON-RX
if FIRST_DATE != FIRST_DATE:
date = datetime.strptime("2020-01-01 00:00:00", "%Y-%m-%d %H:%M:%S")
else:
date = datetime.strptime(FIRST_DATE, "%Y-%m-%d %H:%M:%S").replace(
hour=0, minute=0, second=0
)
# =====!качаем свечи!=====
log_plus(
f"Качаем свечи для акции '{SECID}' с таймфреймом {period} начиная с {date}"
)
real_limit = limit
# цикл до момента когда количество скачанных свечек не будет меньше заданного limit
while limit == real_limit:
try:
df_load = Ticker(SECID).candles(
date=date, till_date=till_date, period=period, limit=limit
)
# обработка ошибки, была на момент написания кода на акции VEON-RX
except:
log_plus(
f"Ошибка от Мосбиржи при получении данных для акции {SECID} с таймфреймом {period}. Продолжаем сбор данных."
)
real_limit = 0
continue # Переход к следующей итерации цикла
# полученный <class 'generator'> преобразуем в DataFrame
df_load = pd.DataFrame(
df_load,
columns=[
"begin",
"end",
"open",
"high",
"low",
"close",
"value",
"volume",
],
)
real_limit = df_load.shape[0] # кол-во полученных строк
if not df_load.empty: # не пустой ли DataFrame
svech = (
df_load.shape[0] + svech
) # кол-во полученных строк по акции всего
# при дневках в колонке "begin" нет %H:%M:%S - исправляем
df_load["begin"] = df_load["begin"] + pd.Timedelta(
hours=0, minutes=0, seconds=0
)
df_load["begin"] = df_load["begin"].dt.strftime("%Y-%m-%d %H:%M:%S")
# получаем время "begin" у последней свечки
date = pd.to_datetime(df_load.iloc[-1]["begin"])
# добавляем к дате период таймфрейм для следующей свечи в следующей итерации цикла
date = date + timeframe
if svech > 1:
# записываем скачанные данные в файл, добавление к существующему в файле содержимому.
df_load.to_csv(
filestocks_path, mode="a", sep="\t", index=False, header=False
)
print(
f"получили данные до {date}. Продолжаем качать ⌛.....", end="\r"
) # отображаем процесс скачивания
if svech > 1:
log_plus(
f"скачано {svech} свечей с таймфреймом {period} для акции '{SECID}'"
)
log_plus(
f"все данные записаны в '{filestocks_path}', размер файла {round(os.path.getsize(filestocks_path)/ (1024 * 1024), 4) } МБ."
)
else:
log_plus(f"Новых данных для акции '{SECID}' не обнаружено")
return
# ---------------КОНЕЦ ФУНКЦИЙ--------------------------------------
# ---------------Начало программы--------------------------------------
console_title("ТАХОМЕТР ТРЕЙДЕРА v1.0")
print("Познавательно-образовательный блог https://алготрейдинг.рф/ ")
print("================================================")
# ASCII art
print(
"""
████████╗ █████╗ ██╗ ██╗ ██████╗ ███╗ ███╗███████╗████████╗██████╗
╚══██╔══╝██╔══██╗██║ ██║██╔═══██╗████╗ ████║██╔════╝╚══██╔══╝██╔══██╗
██║ ███████║███████║██║ ██║██╔████╔██║█████╗ ██║ ██████╔╝
██║ ██╔══██║██╔══██║██║ ██║██║╚██╔╝██║██╔══╝ ██║ ██╔══██╗
██║ ██║ ██║██║ ██║╚██████╔╝██║ ╚═╝ ██║███████╗ ██║ ██║ ██║
╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝"""
)
print()
print("================================================")
print('Программа "ТАХОМЕТР ТРЕЙДЕРА" v1.0')
print("================================================")
# запоминаем время для анализа продолжительности работы программы
start = datetime.now()
folder_path = "historical_data" # папка для хранения данных
filelog_path = os.path.join(folder_path, "logfile.log") # путь к логфайлу
print(
"Привет! Это программа для 'добычи' исторических рыночных данных акций Мосбиржи. \nПрограмма работает с использованием официальной библиотеки биржи 'moexalgo' для AlgoPack API.\nЧтобы ПОДДЕРЖАТЬ автора этой программы не забудьте подписаться на его Телеграм-канал \n\033[30;47m'АЛГОТРЕЙДИНГ на PYTHON'\033[0m - https://t.me/algotrading_step_to_step/."
)
# проверяем наличие папки, если ее нет - создаем
if not os.path.exists(folder_path):
while True:
answer = input("Открыть ссылку на канал? Введите 'y' или 'n': ")
if answer.lower() == "д" or answer.lower() == "y":
print(
"Вы выбрали 'да' - отлично! В браузере открыта ссылка на канал. Спасибо за подписку!"
)
webbrowser.open_new("https://t.me/algotrading_step_to_step/")
break
elif answer.lower() == "н" or answer.lower() == "n":
print(
"Вы выбрали 'нет', очень жаль. \nВсе равно скопируйте себе ссылку на телеграм-канал, в нем будет много полезной и интересной для Вас информации."
)
break
else:
print("Некорректный ввод")
# создаем каталог для хранения файлов с котировками
os.makedirs(folder_path)
with open(filelog_path, "a", encoding="utf-8") as logfile:
log_plus(
f"Каталог '{folder_path}' для хранения файлов с котировками успешно создан"
)
log_plus("Файл логов 'logfile.log' создан")
log_plus("Старт программы")
log_plus(
"Начинаем сканировать список доступных акций Мосбиржи. Подождите пару минут."
)
# настройка входных данных
# берем заведомо старую дату, чтобы найти самые ранние свечи
Data_start = datetime.strptime("1992-01-01 00:00:00", "%Y-%m-%d %H:%M:%S")
market_tools = Market("stocks").tickers() # получаем данные по ВСЕМ акциям
market_tools_df = pd.DataFrame(market_tools) # помещаем в датафрейм
market_tools_df.to_csv(
f"{folder_path}/list_tools.txt", sep="\t", index=False
) # записываем полученные данные в файл
# создаем новую колонку с датой самых ранних данных
market_tools_df["FIRST_DATE"] = ""
# изменяем порядок колонок и сокращаем их количество
market_tools_df = market_tools_df.reindex(
columns=[
"SECID",
"FIRST_DATE",
"LISTLEVEL",
"SECNAME",
]
)
# Цикл для каждого тикера в колонке "SECID" - перебираем все акции
for SECID in tqdm(market_tools_df["SECID"], colour="green"):
try:
# вытаскиваем FIRST_DATE для каждого тикера - дата первой свечи
d1 = pd.DataFrame(
Ticker(SECID).candles(
date=Data_start,
till_date="today",
period=1,
limit=10,
)
).loc[0, "begin"]
# добавляем в колонку FIRST_DATE фактическое значение первой даты d1
current_index = market_tools_df[market_tools_df["SECID"] == SECID].index[0]
market_tools_df.at[current_index, "FIRST_DATE"] = d1
except:
# обработка потенциальной ошибки Мосбиржи, была на момент написания кода на акции VEON-RX
print(
f"\nОшибка от Мосбиржи при получении данных для акции {SECID}. Продолжаем работу."
)
# в случае ошибки добавляем в колонку FIRST_DATE значение NaT
current_index = market_tools_df[market_tools_df["SECID"] == SECID].index[0]
market_tools_df.at[current_index, "FIRST_DATE"] = pd.NaT
# сортируем таблицу по LISTLEVEL в порядке возрастания
market_tools_df.sort_values(
"LISTLEVEL", ascending=True, inplace=True, na_position="first"
)
market_tools_df.to_csv(
f"{folder_path}/list_tools_listlevel.txt", sep="\t", index=False
) # записываем таблицу акций с FIRST_DATE в файл
log_plus(
"Внимание! в папке 'historical_data' имеются файлы 'list_tools.txt' и 'list_tools_listlevel.txt'. \nФайл 'list_tools.txt' содержит справочную информацию по всем акциям Мосбиржи для ознакомления. Расшифровку названий колонок можно посмотреть здесь https://алготрейдинг.рф/moex/column-name-value/ \n\nВАЖНО!!! Файл 'list_tools_listlevel.txt' содержит список всех акции с указанием уровня листинга и первой датой, которая сейчас фактически доступна для скачивания с Мосбиржи. Добыча данных будет происходить по данным этого файла. Вы можете ничего не менять и скачать вообще все доступные данные - это будет долгий процесс. Вы также можете сейчас удалить строки с ненужными акциями. Вы можете изменить даты начала исторических данных по каждой акции в отдельности. Отредактируйте при необходимости этот файл. В дальнейшем программа будет ориентироваться именно на него. Файл никогда не удаляйте! \nДалее мы приступаем к скачиванию данных. Продолжительность зависит от количества акций и первых дат в файле 'list_tools_listlevel.txt'. Для продолжения работы Убедитесь, что файл 'list_tools_listlevel.txt' в папке 'historical_data' закрыт."
)
while True:
answer = input("Для продолжения введите 'y': ")
if answer.lower() == "д" or answer.lower() == "y":
break
else:
print("Вы ввели что-то другое. Пожалуйста, попробуйте снова.")
# спросим еще про таймфреймы
log_plus(
"Мы можем скачать исторические данные с различным таймфреймом. Введите '1' - если Вам нужны минутки, '2' - если нужнен таймфрейм в 10 минут, '3' - если нужнен таймфрейм в 60 минут, '4' - если нужнен таймфрейм в 1 день, или введите '0' для выбора всех таймфреймов."
)
while True:
number_period = input("Введите 1, 2, 3, 4 или 0: ")
if number_period in ["0", "1", "2", "3", "4"]:
number_period = int(number_period)
break
else:
print("Неправильный ответ.")
log_plus(f"Спасибо! Вы ввели: {number_period}")
else:
print("================================================")
log_plus("Старт программы")
log_plus(
"Директория 'historical_data' существует. Это означает, что ранее Вы уже запускали программу. В этом сеансе мы будем докачивать все, что ранее не успели скачать, а также скачаем все вновь появившиеся исторические данные."
)
print("================================================")
# пауза для пользователя, чтобы он понял, что происходит и затем он может нажать Enter
while True:
user_input = input("Нажмите Enter для продолжения работы: ")
if user_input == "":
break
# ищем из файла логов какие таймфреймы ранее были определены пользователем
with open(filelog_path, "r", encoding="utf-8") as file:
content = file.read()
pattern = r"Спасибо! Вы ввели: ([0-4])"
poisk = re.findall(pattern, content)
if poisk:
number_period = int(poisk[-1])
else:
number_period = 0 # если не нашли в логах - качаем все теймфреймы
# читаем файл list_tools_listlevel.txt и создаем tuple_list с акциями и датами для скачивания
df_list = pd.read_csv(f"{folder_path}/list_tools_listlevel.txt", sep="\t")
tuple_list = tuple(zip(df_list["SECID"], df_list["FIRST_DATE"]))
# ------------Переменные-константы-----------------------
# Количество записей данных, полученных за один раз. Максимум 50000
limit = 50000
periods = ["D", 60, 10, 1]
timeframes = [
timedelta(hours=24),
timedelta(hours=1),
timedelta(minutes=10),
timedelta(minutes=1),
]
# ------------конец переменных-констант-----------------------
# определяем нужные таймфреймы в зависимости от введенного числа пользователем
if number_period == 1:
period = 1
timeframe = timedelta(minutes=1)
secid_candles() # качаем исторические данные
elif number_period == 2:
period = 10
timeframe = timedelta(minutes=10)
secid_candles()
elif number_period == 3:
period = 60
timeframe = timedelta(hours=1)
secid_candles()
elif number_period == 4:
period = "D"
timeframe = timedelta(hours=24)
secid_candles()
elif number_period == 0:
# Цикл последовательного сбора исторических данных, если по выбору пользователя нужны все таймфреймы
for period, timeframe in zip(periods, timeframes):
secid_candles() # качаем исторические данные
# считаем время работы программы
vremia = datetime.now() - start
days = vremia.days
hours, remainder = divmod(vremia.seconds, 3600)
minutes, seconds = divmod(remainder, 60)
formatted_vremia = ""
if days > 0:
formatted_vremia += f"{days} дней "
if hours > 0:
formatted_vremia += f"{hours} часов "
if minutes > 0:
formatted_vremia += f"{minutes} минут "
if seconds > 0:
formatted_vremia += f"{seconds} секунд"
log_plus(
f"Все доступные данные добыты! Продолжительность сбора информации составляет {formatted_vremia}. Данные сохранены в файлах и находятся в папке 'historical_data'. При повторных запусках программы информация будет обновляться."
)
print("================================================")
log_plus(
"О появлении новых версий программы Вы можете узнать в Телеграм-канале 'АЛГОТРЕЙДИНГ на PYTHON' :https://t.me/algotrading_step_to_step/"
)
print(
"Автор канала делится информацией и ведет блог о том, как с нуля самостоятельно стал изучать Python для алгоритмического трейдинга (создания торгового робота и автоматизированного анализа рынка с целью генерации торговых сигналов)."
)
print("================================================")
while True:
user_input = input(
"Спасибо за использование программы! Работа завершена. Нажмите Enter для выхода."
)
if user_input == "":
break