Всем здоровья и бодрого расположения духа!
В статье «Визуализация рекомендаций Романа Андреева на Python» мы разобрали как можно с помощью нескольких строк кода на Питоне разобрать текст, который выкладывает каждое утро в своем блоге Роман Андреев (далее по тексту Роман) — известный трейдер и блогер (или наоборот), и отобразить эти рекомендации в виде уровней и зон на графиках. В этом топике я покажу способ для извлечения информации из графических изображений с помощью технологий компьютерного зрения (но без использования нейронных сетей) на примере таблиц-рекомендаций из блога Романа Андреева.
Надеюсь, что я не напугал читателей термином «компьютер вижн», скоро вы поймете, что это просто. И что любой юный прогер может написать код для распознавания внешними камерами номеров автомобилей, который впоследствии возненавидят все автолюбители мегаполисов, а МАДИ и ГИБДД будут собирать со всех нас миллиардные штрафы для поддержания собственных иерархий для улучшения безопасности движения. Се ля ви...
Нам понадобятся следующие ингредиенты:
1. Быть другом Романа Андреева (в хорошем смысле слова) на Смартлабе, чтобы иметь доступ к его блогу.
2. Любая среда Питон (я использовал Jupyter Notebook Anaconda 3)
3. Установленный в Питоне пакет OpenCV — библиотека алгоритмов компьютерного зрения и обработки изображений с открытым кодом. Установка заняла несколько секунд командой $pip install opencv-python. Маленький лайфхак, про который постараюсь подробно рассказать в будущих топиках. Если у вас нет Питона, воспользуйтесь халявой на Google Colab. Там вам предоставят бесплатный Питон в браузере, уже установленные библиотеки, в том числе OpenCV, халявные вычисления на GPU для нейросетей и много других плюшек. Взамен потребуют, как обычно — всего лишь вашу душу:)
4. Набор шаблонов (графических изображений цифр, слов и знаков) для распознавания (ссылка). Папка называется RomanAndreev и содержит в себе две папки. Папка ImageSamples — в ней собраны шаблоны — png-файлы слов, цифр и знаков для распознавания, будем называть их токенами. Я заранее подготовил шаблоны, затратив на это время, сопоставимое с временем написание кода. Выглядит так:
Папка Data. В этой папке собраны графические файлы с рекомендациями (примерно с конца сентября, исключая дни, когда Роман лакомился муксуном и нельмой в нефтедобывающих краях). И в нее вы будете копировать новые рекомендации (правая кнопка мыши на табличке — «Сохранить картинку как»), из которых мы будем извлекать информацию для каждого тикера.
Итак, исходные данные в сборе, переходим к решению.
Сама по себе задача поиска текста на изображениях не является тривиальной, особенно если нужно понять смысл текста (семантику). Но в нашем случае все оказывается гораздо проще. Сходу приведу кусок кода, который распознАет цифру 8(восемь) в файле рекомендации от 1 декабря 2020 года и отметит ее местоположение на изображении.
import cv2 as cv import numpy as np template = cv.imread('RomanAndreev\\ImageSamples\\8.png', 0) #читаем шаблоц цифры "8" img_rgb = cv.imread('RomanAndreev\\Data\\2020-12-01.png') #Читаем изображение рекомендации от 1 декабря 2020 года img_gray = cv.cvtColor(img_rgb, cv.COLOR_BGR2GRAY) #Делаем изображение черно-белым в оттенках серого img_gray = cv.GaussianBlur(img_gray, (3, 3), 0) #Слегка размываем изображение с помощью фильтра Гаусса w, h = template.shape[::-1] #Определяем ширину и высоту шаблона res = cv.matchTemplate(img_gray,template,cv.TM_CCOEFF_NORMED) #Проходим шаблоном по изображению в поисках совпадений threshold = 0.8 #Задаем порог, при превышении которого шаблон считается найденным на изображении loc = np.where( res >= threshold) #Таких мест на изображении может быть много, записываем их координаты в матрицу #Рисуем на основном изображении рамочки, где мы нашли шаблон for pt in zip(*loc[::-1]): cv.rectangle(img_rgb, pt, (pt[0] + w, pt[1] + h), (0,0,255), 1) cv.imwrite('res8.png',img_rgb) #Записываем результат в файлПосле выполнения кода (CTRL+Enter) в файл res8.png будет выглядеть так:
Так вот как камеры определяют номера автовладельцев-нарушителей(но нам бояться нечего — мы все ездим по правилам)!
Понятно, что распознавание не идеально, и как мы видим, за восьмерку были приняты заглавные буквы B-латинская и В-кириллица. От восьмерки еще отличить их можно, но между собой — никак...
Ничего страшного, вспомним классику. В х/ф «Особенности национальной рыбалки» между двумя персонажами состоялся такой диалог: — Весной ящик коньяка со сходен уронили. Когда день рождения начальника тыла праздновали. Так они в мути у военного пирса его отыскали и подняли. — А там глубоко? — Глубоко. — Достанут! — Очень глубоко. — Все равно достанут. Когда знают чего и сколько утопили — достанут!
Вот так и мы, в этой задаче знаем, чего и сколько искать. Для этого выделим по контурам названия инструментов (тикеров), ключевые слова «лонг» и «шорт», а также цифры от 0 до 9 и запятую, и сохраним их в качестве шаблонов — назовем их токенами. Они лежат в папке ImageSamples.
Принцип распознавания следующий: сначала мы находим изображение тикера (например 'Ri-') на основном изображении. Затем движемся вправо по горизонтали, чтобы найти токены «лонг» или «шорт», далее снова движемся вправо в поиске цифр и запятых, если очередная цифра расположена на расстоянии от предыдущей, значит в этом месте начинается новое число.
В коде, который представлен ниже основная функция StartRunning('2020-12-07'), принимает строку в виде ГГГГ-ММ-ДД, соответствующую дате рекомендации и имени png-файла изображения с таблицей 2020-12-07.png, который вы сохранили в папку Data. Фактически функция распознает то, что находится на изображении и сохраняет эту информацию уже в формате csv, в файле ГГГГ-ММ-ДД.csv. Затем читает этот файл в датафрейм Pandas и возвращает его для дальнейшей обработки.
Код с подробными комментариями:
#©2020 by Evgeny Shibaev. А пользуются теперь НУ ВООБЩЕ ВСЕ! #Для установки OpenCV воспользуйтесь командой $ pip install opencv-python #Код для распознавания на графическом изображении чисел, слов и знаков препинания по образцам. Для примера взяты #изображения рекомендаций по различным финансовым инструментам из блога Романа Андреева на Смартлабе. #В результате выполнения кода мы преобразуем картинку в дата фрейм, соответствующий содержанию рекомендации. import cv2 as cv import numpy as np import pandas as pd import csv import os.path from matplotlib import pyplot as plt #Список инструментов (тикеров) ticker_names = ['BR-','GD-','RI-','Si-','MICEX','Eu-','VTBR','GAZP','GMKN','LKOH','MTSS','ROSN','RTKM','SBER','SBERP', 'CHMF','SNGS','SNGSP','FEES'] #tokens - список имен png-файлов, содержащих изображения символов и слов, которые нужно найти на изображении (без учета #имен тикеров):['0','1','2','3','4','5','6','7','8','9','comma','long','short']. Файлы хранятся в папке \\ImageSamples\\ tokens = [str(digit) for digit in range(10)] + ['comma', 'long', 'short'] #Templates file names #folder = папка в Питоне, содержащая папку Data с .png-файлами рекомендаций и папку ImageSamples с образцами цифр, #знаков и слов для распознавания. folder = 'RomanAndreev' # locations это словарь, ключами которого будут имена токенов, а значениями список координат, где эти # токены были выявлены на основном изображении locations = {} #Порог, при превышении которого образец считается найденным на основном изображении. С порогом можно "поиграть", #если какой-то шаблон не идентифицтруется. Стандартно порог=0.8 В нашем случае эмпирически найдено 0.789. #На десяти тестовых файлах не было ни одной ошибки при распознавании threshold = 0.789 #Основная функция для запуска. Извлекает из изображения данные, сохраняет их в формате csv и возвращаетв датафрейм # date - строка вида '2020-11-30', дата в формате ГГГГ-ММ-ДД. Соответствует имени файла изображения в папке Data def StartRunning (date): #Файл куда вы сохранили картинку с рекомендациями по тикерам image_file = f'{folder}\\Data\\{date}.png' #Проверяем не забыли ли вы сохранить изображение if not os.path.exists(image_file): print(f'No such file: {image_file}') return False #Читаем .png файл из которого будем извлекать информацию img_rgb = cv.imread(image_file) #Делаем изображение черно-белым в оттенках серого - засеряем (наверное от слова "засеря") img_gray = cv.cvtColor(img_rgb, cv.COLOR_BGR2GRAY) #Определяем координаты нужных нам цифр, знаков препинания и слов на изображении, т.е. заполняем словарь locations GetTokenLocations(img_gray) #По каждому тикеру записываем строку с рекомендациями в файл 'ГГГГ-ММ-ДД.csv' file = f'{folder}\\Data\\{date}.csv' with open(file, mode='w', encoding='utf-8') as f: #Для записи информации в CSV файл создаем объект writer fw = csv.writer(f) #для просмотра в EXCEL добавьте delimiter = ';', lineterminator='\r' fw.writerow(['Date', 'Tiker', 'Posa', 'Open', 'Reverse', 'PL']) #По каждому тикеру записываем строку с рекомендациями в файл 'ГГГГ-ММ-ДД.csv' for ticker in ticker_names: fw.writerow(GetTikerInfo (date, ticker, img_gray)) #Для проверки, что все чики-пуки, зачтём данные из csv файла в датафрейм Пандас df df = pd.read_csv(file, index_col = 'Tiker') # print(df) return df #Функция определяет координаты найденных токенов на изображении image и сохраняет их в locations def GetTokenLocations (image): #Для каждого токена for token in tokens: #Читаем из файла образец изображения токена в template template = cv.imread(f'{folder}\\ImageSamples\\{token}.png',0) #Собственно "волшебство" распознавания: матрица, содержащая степень похожести образца с куском основного #изображения. Используем небольшое "Гауссово размытие" - изображение становится как будто вы забыли надеть очки res = cv.matchTemplate(cv.GaussianBlur(image, (3, 3), 0),template,cv.TM_CCOEFF_NORMED) #Сохраняем в словаре, для каждого токена координаты точек, где совпадение превышает заданный порог locations[token] = np.where(res >= threshold) #Функция для определения рекомендации по тикеру, возвращает список типа: ['2020-11-30', 'MICEX', 'long', '2758', '3112'] def GetTikerInfo (date, tiker_name, img_gray): #Читаем в template изображение тикера из файла template = cv.imread(f'{folder}\\ImageSamples\\{tiker_name}.png',0) w, h = template.shape[::-1] res = cv.matchTemplate(cv.GaussianBlur(img_gray, (3, 3), 0),template,cv.TM_CCOEFF_NORMED) #Определяем координаты наилучших совпадений с образцом loc = np.where(res >= threshold) #Находим координату Y, чтобы значение было не только выше порога но и максимальным. #Таким образом мы привязали Y к строке, в которой расположена информация о тикере Y = loc[0][np.argmax(res[loc], axis = 0)] #Одному и тому же изображению могут соответствовать несколько соседних точек, особенно если шрифт жирный, #мы впоследствии оставим только одну из них, но сейчас сделаем окрестность dY из ближайших точек dY = (Y, Y+1, Y-1) #token_pos - словарь, ключами которого являются токены, а значениями - списки координат, соответствующих этим токенам #У одного токена может быть несколько координат, например для значения 48,88 у токена '4'и ',' - будет по одной #координате, а токена '8' - три. token_pos = {} rightx, posa = 0, 0 #Инициализация переменных. Описание далее. #Для каждого токена определяем его позицию в окрестностях координаты Y (в строке) и оставляем только одну for token in tokens: #loop only for tokens for pt in zip(*locations[token][::-1]): if pt[1] in dY: #Для координаты Х также создаем окрестность и проверяем, чтобы одному токену соответствовала только одна #координата Х dX = (pt[0], pt[0]+1, pt[0]-1) #Этим выражением исключаем "лишние"значения координат Х для одного токена if not (any(map (lambda key: key in dX, token_pos))): #Если мы нашли изображения слов 'шорт' или 'лонг', то запоминаем позицию в posa, а также точку (rightx) #от которой будем "искать" вправо собственно цену открытия позиции и ее реверса. if token in ('short','long'): posa = token rightx = pt[0] token_pos[pt[0]]= token #После окончания цикла инициализируем строку значением ticker и posa через пробел (она содержит позицию по тикеру) price_str = date + ' ' + tiker_name + ' ' + posa #indent - задает расстояние в пикселях между токенами (буквами или цифрами), в случае превышения которого #мы считаем что, следующий найденный токен будет началом нового слова (или числа) indent = 30 #pixels oldx = rightx #Сортируем найденные в строке токены по возрастанию координаты Х, не включая те токены, которые находились левее #столбца "Позиция" на изображении. Т.к. слева могут попадаться цифры в наименовании тикера, например Ri-12.20 for item in [i for i in sorted(token_pos.items()) if i[0] > rightx]: if item[0] > oldx + indent: price_str = price_str + ' ' + item[1] else: price_str = price_str + item[1] oldx = item[0] #После окончания цикла строка price_str будет примерно такой "short 47comma53 49comma1". Заменим в ней 'comma' #на '.', затем разделяем на пробелы получим список с рекомендацией типа:['short', '47.53', '49.1'] return price_str.replace('comma', '.').split() #Если вы нашли ошибку при распознавании, например пропущена какая-то цифра (что было редко) или цифра распозналась #неправильно (такого не было), то есть два способа исправить. Первый простой - откройте блокнотом csv-файл в папке Data и #исправьте вручную. Второй - более сложный, но позволит избежать подобной ошибки в будущем: откройте основное изображение # графическим редактором, выделите на основном изображении ту цифру, которая была пропущена, например 8, обрежьте #изображение до размеров остальных шаблонов и сохраните в файл под именем '8.png' в папку ImageSamples поверх #старого шаблона. Затем перезапустите основную функцию и проверьте результат распознавания res=StartRunning('2020-12-07') res
Для чего я сделал промежуточное сохранение в csv формат? Очень редко, но встречаются ошибки при распознавании, возможно из-за того, что порог распознавания treshold оказался чуть выше найденного значения и токен не попадет в список (будет просто пропущен). В этом случае откройте файл ГГГГ-ММ-ДД.csv обычным блокнотом, вставьте пропущенную цифру и сохраните изменения. Вторая причина — все мы люди, и Роман может ошибиться (нет, не в направлении движения рынка) — просто по невнимательности. В этом случае, любая не стыковка сразу будет видна на графиках. Я приведу пример ошибочно указанного направления для Eu-12.20 в конце статьи.
После того, как мы извлекли нужную нам информацию из изображения, ее нужно представить графически. Данные для отображения графиков мы берем с сайта Финама. Сам код детально описан в предыдущем топике. В него лишь добавлены тикеры 'Si-','RI-','Eu-','GD-' и 'BR-' и их коды для декабрьских контрактов (нефть — январь). При переходе на новые контракты исправьте коды тикеров, зайдя на сайт Финама. Имена тикеров должны соответствовать именам в переменной ticker_names.
from urllib.parse import urlencode from urllib.request import urlopen from datetime import datetime, timedelta import pandas as pd FINAM_URL = "http://export.finam.ru/" # сервер, на который стучимся #каждому таймфрейму на Финаме соответствует цифровой код: periods={'tick': 1, 'min': 2, '5min': 3, '10min': 4, '15min': 5, '30min': 6, 'hour': 7, 'daily': 8, 'week': 9, 'month': 10} #каждому символу Финам присвоил цифровой код: symbols={'Si-':502420,'RI-':502418,'Eu-':893255,'GD-':924737,'BR-':926134,'S&P':13944,'USDRUB':901,'ED':83,'GD':18953,'MICEX':420450,'BZ':19473,'ABRD':82460,'AESL':181867,'AFKS':19715,'AFLT':29,'AGRO':399716,'AKRN':17564,'ALBK':82616,'ALNU':81882,'ALRS':81820,'AMEZ':20702,'APTK':13855,'AQUA':35238,'ARMD':19676,'ARSA':19915,'ASSB':16452,'AVAN':82843,'AVAZ':39,'AVAZP':40,'BANE':81757,'BANEP':81758,'BGDE':175840,'BISV':35242,'BISVP':35243,'BLNG':21078,'BRZL':81901,'BSPB':20066,'CBOM':420694,'CHEP':20999,'CHGZ':81933,'CHKZ':21000,'CHMF':16136,'CHMK':21001,'CHZN':19960,'CLSB':16712,'CLSBP':16713,'CNTL':21002,'CNTLP':81575,'DASB':16825,'DGBZ':17919,'DIOD':35363,'DIXY':18564,'DVEC':19724,'DZRD':74744,'DZRDP':74745,'ELTZ':81934,'ENRU':16440,'EPLN':451471,'ERCO':81935,'FEES':20509,'FESH':20708,'FORTP':82164,'GAZA':81997,'GAZAP':81998,'GAZC':81398,'GAZP':16842,'GAZS':81399,'GAZT':82115,'GCHE':20125,'GMKN':795,'GRAZ':16610,'GRNT':449114,'GTLC':152876,'GTPR':175842,'GTSS':436120,'HALS':17698,'HIMC':81939,'HIMCP':81940,'HYDR':20266,'IDJT':388276,'IDVP':409486,'IGST':81885,'IGST03':81886,'IGSTP':81887,'IRAO':20516,'IRGZ':9,'IRKT':15547,'ISKJ':17137,'JNOS':15722,'JNOSP':15723,'KAZT':81941,'KAZTP':81942,'KBSB':19916,'KBTK':35285,'KCHE':20030,'KCHEP':20498,'KGKC':83261,'KGKCP':152350,'KLSB':16329,'KMAZ':15544,'KMEZ':22525,'KMTZ':81903,'KOGK':20710,'KRKN':81891,'KRKNP':81892,'KRKO':81905,'KRKOP':81906,'KROT':510,'KROTP':511,'KRSB':20912,'KRSBP':20913,'KRSG':15518,'KSGR':75094,'KTSB':16284,'KTSBP':16285,'KUBE':522,'KUNF':81943,'KUZB':83165,'KZMS':17359,'KZOS':81856,'KZOSP':81857,'LIFE':74584,'LKOH':8,'LNTA':385792,'LNZL':21004,'LNZLP':22094,'LPSB':16276,'LSNG':31,'LSNGP':542,'LSRG':19736,'LVHK':152517,'MAGE':74562,'MAGEP':74563,'MAGN':16782,'MERF':20947,'MFGS':30,'MFGSP':51,'MFON':152516,'MGNT':17086,'MGNZ':20892,'MGTS':12984,'MGTSP':12983,'MGVM':81829,'MISB':16330,'MISBP':16331,'MNFD':80390,'MOBB':82890,'MOEX':152798,'MORI':81944,'MOTZ':21116,'MRKC':20235,'MRKK':20412,'MRKP':20107,'MRKS':20346,'MRKU':20402,'MRKV':20286,'MRKY':20681,'MRKZ':20309,'MRSB':16359,'MSNG':6,'MSRS':16917,'MSST':152676,'MSTT':74549,'MTLR':21018,'MTLRP':80745,'MTSS':15523,'MUGS':81945,'MUGSP':81946,'MVID':19737,'NAUK':81992,'NFAZ':81287,'NKHP':450432,'NKNC':20100,'NKNCP':20101,'NKSH':81947,'NLMK':17046,'NMTP':19629,'NNSB':16615,'NNSBP':16616,'NPOF':81858,'NSVZ':81929,'NVTK':17370,'ODVA':20737,'OFCB':80728,'OGKB':18684,'OMSH':22891,'OMZZP':15844,'OPIN':20711,'OSMP':21006,'OTCP':407627,'PAZA':81896,'PHOR':81114,'PHST':19717,'PIKK':18654,'PLSM':81241,'PLZL':17123,'PMSB':16908,'PMSBP':16909,'POLY':175924,'PRFN':83121,'PRIM':17850,'PRIN':22806,'PRMB':80818,'PRTK':35247,'PSBR':152320,'QIWI':181610,'RASP':17713,'RBCM':74779,'RDRB':181755,'RGSS':181934,'RKKE':20321,'RLMN':152677,'RLMNP':388313,'RNAV':66644,'RODNP':66693,'ROLO':181316,'ROSB':16866,'ROSN':17273,'ROST':20637,'RSTI':20971,'RSTIP':20972,'RTGZ':152397,'RTKM':7,'RTKMP':15,'RTSB':16783,'RTSBP':16784,'RUAL':414279,'RUALR':74718,'RUGR':66893,'RUSI':81786,'RUSP':20712,'RZSB':16455,'SAGO':445,'SAGOP':70,'SARE':11,'SAREP':24,'SBER':3,'SBERP':23,'SELG':81360,'SELGP':82610,'SELL':21166,'SIBG':436091,'SIBN':2,'SKYC':83122,'SNGS':4,'SNGSP':13,'STSB':20087,'STSBP':20088,'SVAV':16080,'SYNG':19651,'SZPR':22401,'TAER':80593,'TANL':81914,'TANLP':81915,'TASB':16265,'TASBP':16266,'TATN':825,'TATNP':826,'TGKA':18382,'TGKB':17597,'TGKBP':18189,'TGKD':18310,'TGKDP':18391,'TGKN':18176,'TGKO':81899,'TNSE':420644,'TORS':16797,'TORSP':16798,'TRCN':74561,'TRMK':18441,'TRNFP':1012,'TTLK':18371,'TUCH':74746,'TUZA':20716,'UCSS':175781,'UKUZ':20717,'UNAC':22843,'UNKL':82493,'UPRO':18584,'URFD':75124,'URKA':19623,'URKZ':82611,'USBN':81953,'UTAR':15522,'UTII':81040,'UTSY':419504,'UWGN':414560,'VDSB':16352,'VGSB':16456,'VGSBP':16457,'VJGZ':81954,'VJGZP':81955,'VLHZ':17257,'VRAO':20958,'VRAOP':20959,'VRSB':16546,'VRSBP':16547,'VSMO':15965,'VSYD':83251,'VSYDP':83252,'VTBR':19043,'VTGK':19632,'VTRS':82886,'VZRZ':17068,'VZRZP':17067,'WTCM':19095,'WTCMP':19096,'YAKG':81917,'YKEN':81766,'YKENP':81769,'YNDX':388383,'YRSB':16342,'YRSBP':16343,'ZHIV':181674,'ZILL':81918,'ZMZN':556,'ZMZNP':603,'ZVEZ':82001} # Функция запрашивает котировки с сервера экспорта данных Финама по инструменту для заданного таймфрейма за последние # period_days дней и возвращает соответствующий датафрейм def GetCandles (ticker, time_frame, period_days): period=periods[time_frame] #Выбор из: 'tick': 1, 'min': 2, '5min': 3, '10min': 4, '15min': 5, '30min': 6, 'hour': 7, 'daily': 8, 'week': 9, 'month': 10 market = 0 #91 24 #можно не задавать. Это рынок, на котором торгуется бумага. Для акций работает с любой цифрой. Другие рынки не проверял. # Текущий момент времени end_date = datetime.today() # Время period_days дней назад start_date = end_date - timedelta(days = period_days) #Все параметры упаковываем в единую структуру. Здесь есть дополнительные параметры, кроме тех, которые заданы в шапке. См. комментарии внизу: params = urlencode([ ('market', market), #на каком рынке торгуется бумага ('em', symbols[ticker]), #вытягиваем цифровой символ, который соответствует бумаге. ('code', ticker), #тикер нашей акции ('df', start_date.day), #Начальная дата, номер дня (1-31) ('mf', start_date.month - 1), #Начальная дата, номер месяца (0-11) ('yf', start_date.year), #Начальная дата, год ('from', start_date), #Начальная дата полностью ('dt', end_date.day), #Конечная дата, номер дня ('mt', end_date.month - 1), #Конечная дата, номер месяца ('yt', end_date.year), #Конечная дата, год ('to', end_date), #Конечная дата ('p', period), #Таймфрейм ('f', ticker), #Имя сформированного файла ('e', ".csv"), #Расширение сформированного файла ('cn', ticker), #ещё раз тикер акции ('dtf', 1), #В каком формате брать даты. Выбор из 5 возможных. См. страницу https://www.finam.ru/profile/moex-akcii/sberbank/export/ ('MSOR', 0), #Время свечи (0 - open; 1 - close) ('mstime', "on"), #Московское время ('mstimever', 1), #Коррекция часового пояса ('sep', 1), #Разделитель полей (1 - запятая, 2 - точка, 3 - точка с запятой, 4 - табуляция, 5 - пробел) ('sep2', 1), #Разделитель разрядов ('datf', 1), #Формат записи в файл. Выбор из 6 возможных. ('at', 1)]) #Нужны ли заголовки столбцов url = FINAM_URL + ticker + ".csv?" + params #собственно URL сформированного запроса #Создаем датафрейм candles с котировками candles = pd.read_csv(url) #Добавляем в датафрейм столбец 'DT', который будет содержать время каждой свечи в формате datetime. #Формируем его из столбцов '<DATE>'и '<TIME>' candles['DT'] = list(map(lambda d,t: ToDatetime(d,t), candles['<DATE>'], candles['<TIME>'])) #Возвращает Датафрейм Пандас со свечами, соответствующими запросу return candles #Преобразует число (или строку) вида 20201030 и строку вида '12:15:00' в объект datetime.datetime(2020, 10, 30, 12, 15) def ToDatetime (date_num, time_hhmmss): return datetime.strptime(str(date_num) + time_hhmmss, '%Y%m%d%H:%M:%S') #Преобразует строку (или число) вида "20201102" в дату (формат datetime) def ToDate (date_yyyymmdd): return datetime.strptime(str(date_yyyymmdd), '%Y%m%d').date() #Преобразует строку вида "2020-11-02" в дату (формат datetime) def ToDateYYYYMMDD (date_yyyy_mm_dd): return datetime.strptime(date_yyyy_mm_dd, '%Y-%m-%d').date() SBER = GetCandles ('SBER', "30min", 10) SBERВы можете, для примера, запустить этот код независимо, в результате получите датафрейм, содержащий 30 минутные свечи акций Сбербанка за последние 10 дней. Этот код мы используем для отображения графиков.
import time import os import matplotlib.pyplot as plt #Список инструментов, которые анализирует Роман. Тикер - токен: тикер это торгуемый актив, а токен - его привычное название. # Ключ в словаре tickers ДОЛЖЕН СТРОГО СООТВЕТСТВОВАТЬ ключу в словаре symbols (в модуле загрузки котировок с Финама) tickers = {'RI-' : 'RI-12.20', 'Si-' : 'Si-12.20', 'BR-' : 'BR-1.21', 'GD-' : 'GD-12.20', 'MICEX':'Индекс ММВБ', 'Eu-' : 'Eu-12.20', 'VTBR' : 'ВТБ', 'GAZP' : 'Газпром', 'GMKN' : 'ГМК Норильский никель', 'LKOH' : 'Лукойл' 'MTSS' : 'MTC', 'ROSN': 'Роснефть', 'RTKM': 'Ростелеком', 'SBER': 'Сбербанк, об', 'SBERP': 'Сбербанк, пр', 'CHMF' : 'Северсталь', 'SNGS' : 'Сургутнефтегаз, об', 'SNGSP': 'Сургутнефтегаз, пр', 'FEES' : 'ФСК ЕЭС' } #################################### Помощники для построения графиков ########################################## #Определяет начальную и конечную позицию Х (по индексу свечей) для заданной даты. Пригодится при отрисовке ценовых уровней def DateX (date, candles): #Цикл по датам в свечах, результат - список X-координат, соответствующих заданной дате xpositions = [index for index, row in candles.iterrows() if row['DT'].date() == date] #Возвращает список - пару начальная координата Х и конечная координата Х для заданной даты на графике if xpositions == []: return [len(candles)-1, len(candles)] #На случай если за текущую дату нет еще свечей return [xpositions[0], xpositions[-1]] #Рисует метки дат на оси Х def PlotDatesX (fig, candles): #Составляем список дат (только уникальные даты) из столбца DT. Они будут метками на оси Х. Сортировка по датам #обязательна, т.к. при создании множества(set) даже из отсортированного списка, множество может не сохранить порядок списка dates = sorted(set(map(lambda dt: datetime.date(dt), candles['DT']))) #Создаем список координат Х для каждой метки (даты). Нам нужна только первая позиция - [0]. xlabel = [DateX(d, candles)[0] for d in dates] #Рисуем ось Х, разделенную по датам fig.set_xticklabels([dt.strftime("%d %b") for dt in dates]) fig.set_xticks(xlabel) return dates, xlabel #Рисует основной график def draw_candles(ticker, candles): #Добавим на график несколько ЕМА-средних candles['ema100'] = pd.Series.ewm(candles['<CLOSE>'], span=100).mean() candles['ema50'] = pd.Series.ewm(candles['<CLOSE>'], span=50).mean() candles['ema20'] = pd.Series.ewm(candles['<CLOSE>'], span=20).mean() plt.style.use('ggplot') #'seaborn-paper' #Отображаем график по цене закрытия свечей и ЕМА-шки fig = candles.plot(y=['<CLOSE>', 'ema50', 'ema20', 'ema100'], figsize=(25,16)) #Добавляем заголовок fig.set_title('График ' + tickers[ticker]) #Рисуем шкалу с датами PlotDatesX (fig, candles) #Если в числе присутствует десятичная точка и после нее нет значащих цифр, делаем из числа целое - "чисто для красоты" def PointOff(valfloat): if valfloat % 1 == 0: return int(valfloat) return valfloat #Рисует уровни открытой позиций и уровни переворота позиции на заданную дату, для свечек (candles) тикера. Где Posa - # текущая позиция 'long' или 'short'на начало дня, price - уровень открытия позиции, reverse - уровень переворота позиции #Также, отображает зеленым и розовым цветами (с заливкой) уровни предполагаемой прибыли или убытка относительно #текущей позиции если она будет закрыта (реверсирована) def draw_levels (datetime_date, candles, posa, price, reverse): DX = DateX(datetime_date, candles) x = len(candles) #Координата Х правого края графика last_price = PointOff(candles.iloc[-1]['<CLOSE>']) #цена последней сделки posanum = -1 if posa == 'long' else 1 equity_color = 'lightgreen' if posanum * (price - reverse) > 0 else 'lightcoral' plt.fill([DX[0], DX[0], DX[1], DX[1]], [price, reverse, reverse, price], alpha = 0.2, color = equity_color) plt.text(x, last_price, str(last_price), color = 'white', verticalalignment='center', bbox={'facecolor': 'slategray', 'pad': 2}) posa_color = 'seagreen' if posa == 'long' else 'firebrick' plt.plot(DX, [price, price], color = posa_color, linewidth = 3) reverse_color = 'red' if posa == 'long' else 'seagreen' plt.plot(DX, [reverse, reverse], color = reverse_color, linewidth = 0.5) plt.text(DX[0], price, f'{price}', color = 'white', verticalalignment='center', bbox={'facecolor': posa_color, 'pad': 2}) v_aligment = 'top' if posa == 'long' else 'bottom' plt.text(DX[0], reverse, f'{reverse}', color = reverse_color, verticalalignment=v_aligment) def start_function(): folder = 'RomanAndreev/Data/' days_past = 30 #за какое количество торговых сессий от сегодняшней показывать позицию days_for_chart = 45 #за какое количество календарных дней строить общий график time_frame = 'hour' #какой тайм-фрейм использовать для графика daily_df = {} #Словарь, ключами которого будут даты рекомендаций, а значениями датафреймы с самими рекомендациями для каждого тикера на заданную дату for file in [f for f in os.listdir('RomanAndreev/Data/') if '.csv' in f][-days_past:]: filedate = file.split('.')[0] daily_df[filedate] = pd.read_csv(f'RomanAndreev/Data/{file}', index_col = 'Tiker') print(file, 'load') #Проход по всем тикерам в списке рекомендаций print('Please, wait...') for ticker in tickers: last_levels = [] #в этой переменной после окончания цикла будут уровни текущего дня, которые мы подпишем на графике last_advice = '' #а в этой переменной рекомендации на текущий день #Читаем в датафрейм candles свечки с сайта Финама candles = GetCandles (ticker, time_frame, days_for_chart) time.sleep(0.5) #делаем небольшую задержку в запросах к серверу котировок, чтобы нас Финам не забанил #Рисуем свечи draw_candles(ticker, candles) for current_date in daily_df: df = daily_df[current_date] draw_levels(ToDateYYYYMMDD(current_date), candles, df.loc[ticker]['Posa'], PointOff(df.loc[ticker]['Open']),PointOff(df.loc[ticker]['Reverse'])) return daily_df start_function() TrueЕсли вы все сделали правильно то в результате получите примерно следующие графики:
Почему уелыкий гуру не выйдет на ЛЧИ и не докажет умение торговать?))
Ах да был два года назад что то вроде -20% показал.
Больше не ногой,)))
сизифов труд
И комментов было… тут же… эээ я просто года три не заходил на рынок вообще… и тут такое беда… автору статьи БРАВО!!!
Мощно).
Ещё сюда парсер просится, который картинки сам парсит из всех постов.
У меня б больше времени ушло, наверно).
Вот только не ясно зачем делать blur, если он заведомо ухудшает качество и почему нет ресайза. Получается шаблон мы ищем определенного размера, так?
Но особо там придумывать нечего — масштабируете спектр картинки в кратное число раз (промежуточные масштабы тоже сработают кое-как), и гуляете по своему изображению с набором спектральных шаблонов.
Остальное зависит от того, что ищете — можно сразу на изображении пытаться выделить области букв, а потом под размер каждой области подбирать масштаб шаблона.
Там всё ой как не просто, поэтому я и спросил вас)
Тоже что ли заняться Питоном, а то отстал от жизни давно…
daily_df[filedate] = pd.read_csv(f'RomanAndreev/Data/{file}', index_col = 'Tiker')
NameError: name 'pd' is not defined
Кстати в списке инструментов tickers = {} после 'LKOH': 'Лукойл' нет запятой…
После Лукойла должна быть запятая. Спасибо за замечания.
Мне понравилось, на Python никогда не пытался ничего делать Здорово.
Может попробую :)
С точки зрения практического использования то конечно реверсивные системы спорное дело... А вот уровни по второй картинке и таблицы в комментариях от Андреева, да еще с выводом на графики Quik посредством обработки подготовленных на Python файлов индикатором Lua было бы наглядно. Я Квик поставил только по просьбе сына присмотреть за его сделками. Последний раз видел это творение лет «дцать» назад — ну просто слов нет что за архаизм! И где наши самые лучшие в мире программисты?!
Удачи, спасибо за коды.
import numpy as np
>a = np.array([[1,2,3,4],[5,6,7,8],[9,10,11,12]])
print(a)
print(a[::-1])
print(a.shape[::-1])
[[ 1 2 3 4]
[ 5 6 7 8]
[ 9 10 11 12]]
[[ 9 10 11 12]
[ 5 6 7 8]
[ 1 2 3 4]]
(4, 3)
А в нашем случае просто определяем ширину и высоту шаблона template в пикселях.
coordinates.sort() #sort by x
coordinates.sort(key = sort2) # sort by y
for coord in coordinates:
x,y,w,h = coord
print(x,y,w,h)
if y>prev_y+5: #new row if y is changed
recognized_table.append(row)
row = []
crop_img = image[y:y+h, x:x+w]
в последней строке не принимает координаты «string indices must be integers»
Не могу понять в чем дело
если б к этим людям подключить динамо машину… )))
Приветствую, Евгений.
В словаре tickers пропущена запятая в строке с ключом LKOH.
… а еще много странностей, функции возвращают данные, но при вызове эти данные не принимаются, например, PlotDatesX.
… или вот не пойму в юпитере общая область видимости? в draw_candles вызывается PlotDatesX, где с переданной fig производятся манипуляции, но изменения не возвращаются функцией!
В питоне вроде бы должно быть так:
def foo(a):
a = 3 # локальное изменение
if __name__ == '__main__':
a = 1
foo(a)
print(a) # в результате все равно 1. манипуляции в foo не оказывают никакого влияния, ведь изменения не были возвращены и приняты.
В личку написать карма не позволяет, поэтому сюда.
Евгений Шибаев, хороший способ отловить ошибки — собрать проект из кода в статье.