О чем эта статья: В основном, о том, как создать рандомайзер биржевых активов используя данные Московской и Питерской бирж на Python.
Кому не важна реализация то вот готовый код с минимальными инструкциями по запуску. Ну или можно воспользоваться телеграмм ботом для всего того же самого, но в удобном формате (если он не работает, то скорее всего, я перестал платить за админку ????). А в самом низу есть небольшое подведение итогов.
Многие классные проекты начинались с “А что если …?”, моя идея не нова и не претендует на “классность”. Но все же, а что если сделать рандомайзер для акций российского рынка и можно ли на нем заработать в долгую?
Что же, на вопрос “А что если …” я и отвечу в этой статье. А вот про "заработать"... тут сложнее. Но обо всем по порядку.
Пожалуй, начать надо с того, что данная статья не является финансовой рекомендацией. У меня нет финансового образования и богатого финансового опыта. Все что вы прочтете далее основано исключительно на моих наблюдениях, опыте и субъективном мнении. И помните: инвестиции всегда сопряженный с риском.
Начало пути
Зайду издалека. Несколько лет назад я, как и многие обыватели, начал пробовать себя в инвестициях. Используя различные инвестиционные, и не очень, инструменты я периодически вгонял состояние своего баланса в "отрицательный рост", тем самым получая какой-никакой опыт. Что и подтолкнуло к изучению всей этой денежной темы более подробно. В итоге прочитав кучу статей, подписавшись на каналы всякого рода "финансовых гуру", просмотрев множество роликов и даже пройдя на "Нетологии" курсы Сергея Спирина (классный дядька и рассказывает классно) который пропагандирует Asset Allocation. Я понял, что никто не владеет супер методом зарабатывания на биржи, и большая часть вообще не представляет как оно там все устроено. По сути, люди делятся на 2 глобальных вида биржевых игроков: те кто играет в откровенную "угадайку", порой даже диверсифицируя свои портфели в надежде минимизировать риски, и на тех кто играет "нечестно" зарабатывая на инсайдерской информации.
Причем же тут рандом? Во время изучения всей этой “финансовой грамотности” я не раз наталкивался на примеры случайного приобретения активов, которые в долгосроке приносили неплохую прибыль. И конечно вы хотите примеры, их есть у меня:
Обезьянка Лукерья (Наверное, самый известный пример на постсоветском пространстве)
Хомяк инвестор Mr.Goxx
Специально обученные крысы инвесторы
Попугай Ddalgi
Кот Орландо
И это только примеры, которые есть на поверхности, подобных экспериментов множество. Но объединяет их одно (точнее "два"). Во-первых, у всех крайне странная выборка (за исключением хомяка, он шикарен… был). Во-вторых, это все очень похоже на ошибку выжившего.
Исходя из замечаний выше, нам нужно увеличить выборку, значит возьмем весь доступный в России рынок активов (на момент начала написании программы и статьи еще можно было торговать на Питерской бирже “неквалам”). Ну а проверить второй тезис можно только совершив тысячи сделок и создав сотни портфелей, что крайне затратно (да, конечно можно смоделировать, но это не так весело).
И так, для того чтобы проверить прибыльность рандомизированного инвестирования нам необходим генератор случайных активов. По началу, в планах было задействовать для этих целей одну из кошек друга. Но они отказались от предложенной “должности”.
А в свете того, что сейчас каждый второй знает python и пихают его куда можно и куда нельзя. Решено было написать простенький рандомайзер на python.
Немного кода.
Прежде чем изобретать велосипед, нужно понять, а не сделал ли его кто-то другой. Поэтому я начал поиски проектов подходящих по тематике. То ли я плохо искал, то ли собрался делать особо бесполезную программу, но нашел только один пример реализации (этот же проект на GitHab) написанный @@empenoso. Проект написан на JS и выдает только акции МосБиржи. На момент написания программы мне хотелось получать данные не только с Московской, но и с Питерской биржи, также помимо акций получать и другие виды активов, а еще было бы просто замечательно иметь возможность устанавливать лимит по стоимости актива. Так что было решено написать все самостоятельно.
Я решил не нагружать статью обилием кода. Поэтому буду приводить в качестве примера только основные функции реализованного мной класса. Кому интересна вся структура без труда сможет посмотреть код целиков в GitHub-е.
По началу идея была такой:
Каждый месяц инвестировать согласно рандомному выбору 3000руб (примерно 10% от медианной ЗП по России). Смотреть на "Доходность\Потери" в различные промежутки времени и сравнивать с инвестиционными фондами
Чуть позже от идеи пришлось отказаться, ибо для покупок остался только рынок МБ, что на мой взгляд крайне маленькая выборка активов, да и он последнее время... не стабилен. А еще, я решил попробовать инвестировать в крипту (да, время я выбрал просто лучшее). Возможно, чуть позже я пересмотрю свое решение. Но пока так. Да и скрипт уже написан.
Приступим к написанию кода.
Подготовка данных.
Для получения рандомного актива, нужно сначала получить весь список необходимых активов. Начнем с самого простого. МосБиржа - тут есть достаточно не плохой и крайне дружелюбный API который помогает получать любые данные с биржи, к тому же абсолютно бесплатно. Как оказалось это нонсенс, остальные биржи, по большей части требуют плату за предоставление данных, особенно для данных в реальном времени.
Меня на МосБирже интересовали 4 вида актива: акции, ПИФы, ОФЗ, облигации. Их мы и будем доставать из МБ и класть каждый вид актива в отельный JSON файл.
Получаем данные из МосБиржи
import asyncio
import json
import config
import requests
# ссылки на получения данных с API
url_shares = 'https://iss.moex.com/iss/engines/stock/markets/shares/boards/TQBR/securities.json?first=350'
url_bpif = 'https://iss.moex.com/iss/engines/stock/markets/shares/boards/TQTD/securities.json?first=500'
url_ofz = 'https://iss.moex.com/iss/engines/stock/markets/bonds/boards/TQOB/securities.json?first=200'
url_bonds = 'https://iss.moex.com/iss/engines/stock/markets/bonds/boards/TQCB/securities.json?first=3000'
async def _download_stock_market_data(url, file_path):
with requests.get(url) as response:
if response.status_code == 200:
with open(file_path, 'w') as file:
json.dump(response.json(), file)
async def download_data():
await asyncio.gather(
_download_stock_market_data(url_ofz, config.ofz_file),
_download_stock_market_data(url_bonds, config.bonds_file),
_download_stock_market_data(url_bpif, config.bpif_file),
_download_stock_market_data(url_shares, config.stocks_file),
)
В config.py лежат пути к разным данным программы.
Отлично! Данные для МБ уже есть. (И если говорить откровенно то на момент переписывания этой статьи, получения данных с Питерской биржи особого смысла не имеет. Но будем надеяться на лучшее и верить в то, что очень скоро всем "неквалам" снова позволят торговать адекватными иностранными активами.).
Теперь все простое закончилось и начинаются костыли. Да, верно, необходимо получить активы с СПБ. Адекватного API, который по запросу отдавал бы все, что мне нужно, я не нашел, видимо его и нет. Можно воспользоваться сторонними API, от брокеров, допустим Тинкоффским API. Вот только у них, как правило, будет не весь список активов. Поэтому… как бы это прискорбно не было… придется городить костыли, в данном случаи вытаскивание данных будет реализована через selenium. Единственный способ, который я нашел, для получения данных - это таблица на сайте Санкт-Петербургской биржи, которую можно получить в формате CSV. Так что, сначала происходит попытка получить данную таблицу цивильным requests, и в случае срабатывания тротлинга или просто неуспеха запускаются костыли.
Получаем данные из Санкт-Петербургской биржи
from webdriver_manager.chrome import ChromeDriverManager
import config
# получение имени последнего установленного файла
def latest_download_file():
path_download = config.path_file / 'spb_data'
os.chdir(path_download)
files = sorted(os.listdir(os.getcwd()), key=os.path.getmtime)
if files:
newest = files[-1]
return newest
else:
raise Exception('dir is empty')
async def download_data():
url_spb = 'https://spbexchange.ru/ru/stocks/inostrannye/Instruments.aspx?csv=download'
with requests.get(url_spb, allow_redirects=True) as r:
# попытка скачивания номральным способом
if r.status_code == 200:
with open(config.spb_file, 'wb') as f:
f.write(r.content)
else:
# попытка скачивания через браузер
option = webdriver.ChromeOptions()
prefs = {"download.default_directory": str(config.path_file / 'spb_data')}
option.add_experimental_option('prefs', prefs)
driver = webdriver.Chrome(ChromeDriverManager().install(), options=option)
driver.get(url_spb)
time.sleep(5)
driver.close()
file = latest_download_file()
if pathlib.Path(file).suffix == '.csv':
os.renames(file, 'SPB.csv')
Теперь у нас есть все доступные данные с СПБ. У них есть два недостатка. Во-первых, тут все в куче, и акции и облигации. Во-вторых, отсутствие стоимости активов, но об этом чуть позднее.
Обработка данных
Самое время заняться вычленением нужной нам информации из уже имеющихся данных.
Начнем, как всегда, с МосБиржи. Тут необходимо объединить все подготовленные json файлы в базу знаний. Я решил что разумно будет разделить все на 2 таблицы. В первой таблицы собрать все акции и ПИФ-ы, во второй все облигации и ОФЗ. Реализуем слияние при помощи pandas. Но прежде чем соединить 2 таблицы из json в одну, необходимо их укоротить, оставив только столбцы, которые потребуются дальше. Я выбрал 5 столбцов: SECID, SECNAME, ISIN, PREVPRICE, BOARDID.
Преобразование json в таблицы
@staticmethod
def _get_short_table(json_dict: dict) -> pandas.DataFrame:
"""
Получение укороченной таблицы, только с необходимыми столбцами
Parameters:
json_dict (dict): json данные таблицы
"""
data = json_dict['securities']['data']
columns = json_dict['securities']['metadata']
df = pandas.DataFrame(data=data, columns=columns)
short_table = df[['SECID', 'SECNAME', 'ISIN', 'PREVPRICE', 'BOARDID']]
return short_table
def _get_mb_stock_exchange(self):
"""
Загрузка данных из json
"""
# open JSON
for file_name in [config.stocks_file, config.bpif_file,
config.ofz_file, config.bonds_file]:
self.file_exist(file_name, msbd)
with open(config.stocks_file, 'r') as file:
shares_json = json.load(file)
with open(config.bpif_file, 'r') as file:
bpif_json = json.load(file)
with open(config.ofz_file, 'r') as file:
ofz_json = json.load(file)
with open(config.bonds_file, 'r') as file:
bonds_json = json.load(file)
# Акции и пифы
self.main_table_stocks_ru = pandas.concat([self._get_short_table(shares_json),
self._get_short_table(bpif_json)],
ignore_index=True)
# Облигации и ОФЗ
self.main_table_bonds_ru = pandas.concat([self._get_short_table(ofz_json),
self._get_short_table(bonds_json)],
ignore_index=True)
Теперь у нас есть 2 таблицы на основе данных из МБ. Дальше необходимо обработать таблицы с СПБ. Тут все немного проще, ибо таблица будет одна, только с акциями. Почему я не учитываю облигации с СПБ? Потому что большая часть не торгуется на общедоступных биржах, да что уж там на биржах, даже на Investing и Yahoo Finance их практически невозможно найти. Но функционал реализован, и кому он необходим всегда сможет просто раскомментировать последние две строчки в функции получения данных с СПБ и немногого подправить код в методе choice()
.
Обработка данных с СПБ
def _get_spb_stock_exchange(self):
"""
Получить данные с питерской биржи """ self.file_exist(config.spb_file, spbd)
spb_table = pandas.read_csv(config.spb_file, sep=';')
# название e_full_name
# код s_RTS_code # ис_код s_ISIN_code # вид актива s_sec_type_name_dop spb_table = spb_table[['s_RTS_code', 's_ISIN_code', 'e_full_name', 's_sec_type_name_dop']]
spb_table.rename(columns={'s_RTS_code': 'SECID', 'e_full_name': 'SECNAME', 's_ISIN_code': 'ISIN'}, inplace=True)
# сортируем в разные таблицы по виду актива (акции, облигации)
spb_table_stock = spb_table[(spb_table.s_sec_type_name_dop == 'Акции')]
self.spb_table_stock = spb_table_stock[['SECID', 'SECNAME', 'ISIN']]
# spb_table_bond = spb_table[(spb_table.s_sec_type_name_dop == 'Облигации')]
# self.spb_table_bond = spb_table_bond[['SECID', 'SECNAME', 'ISIN']]
Просто великолепно. Осталось совсем немного, а именно: выбрать рандомный актив и вывести все это на экран.
Итоговый выбор
Приступим к выбору.
Для начала нам нужно определиться с видом актива: мы хотим получить акцию или облигацию. Затем решить с какой из бирж мы хотим получить актив. И в конце получить рандомный актив, из подготовленной таблицы на предыдущих шагах.
Определяемся с начальными условиями выбора и получаем актив
def choice(self):
"""
Определения актива и биржи с дальнейшим получением конкретного актива
"""
match self.asset:
# Выбор актива
case 'stock':
# выбор биржи
if ('MB' in self.stock_market) & ('SPB' in self.stock_market):
self.choice_table = pandas.concat([self.spb_table_stock,
self.main_table_stocks_ru],
ignore_index=True).sample().values[0]
elif 'MB' in self.stock_market:
self.choice_table = self.main_table_stocks_ru.sample().values[0]
elif 'SPB' in self.stock_market:
self.choice_table = self.spb_table_stock.sample().values[0]
else:
print("O no. You didn't choose any stock market")
raise Exception('stock market not chosen')
case 'bond':
if ('MB' in self.stock_market) & ('SPB' in self.stock_market):
self.choice_table = self.main_table_bonds_ru.sample().values[0]
elif 'MB' in self.stock_market:
self.choice_table = self.main_table_bonds_ru.sample().values[0]
elif 'SPB' in self.stock_market:
raise Exception('At the moment you cannot use bonds from the St. Petersburg Stock Exchange')
else:
print("O no. You didn't choose any stock market")
raise Exception('stock market not chosen')
case _:
raise Exception('incorrect asset name')
# итоговое получение актива
self.asset_choice = self.choice_obj(self.choice_table)
Теперь остается только обработать выбранный актив для дальнейшего его представления в удобном формате. Берем выбранный объект и распределяем его внутренние компоненты по переменным. В конце функции, так же реализован выбор по лимиту, если стоимость акции окажется выше заранее веденного лимита, то рандомный выбор будет повторен до тех пор пока стоимость акции не будет удовлетворять условию. По умолчанию лимита нет, и проверка данного условия не выполняется. Во избежании зацикливания и долгого поиска не рекомендуется ставить лимит ниже 300руб.
Обработка выбранного актива
def choice_obj(self, data: list) -> dict:
"""
Преобразование выбранного актива
Parameters:
data (list): Данные выбранного актива
Returns:
return (dict): Словарь преобразованных данных
"""
try:
data_nan = math.isnan(data[3])
except IndexError:
data_nan = True
if data_nan:
price = self._get_current_price(data[0])
price_str = f'({price["stock_current_prise"]} {price["currency"]}) {price["rub_prise"]:.{2}f} руб.' price_ru = price["rub_prise"]
price_increase = price["price_increase"]
else:
price = data[3]
price_ru = data[3]
price_str = f'{data[3]} руб.' price_increase = self.percentage_change(price_ru, data[0], data[4])
quantity = 1
if self.limit:
if self.limit >= price_ru:
int_div = int(self.limit // price_ru) + 1
quantity = random.choice(range(1, int_div))
return {
'sec_id': data[0],
'full_name': data[1],
'isn': data[2],
'price': price,
'price_str': price_str,
'price_ru': price_ru,
'quantity': quantity,
'price_increase': price_increase
}
При обработке выбранной акции есть один нюанс, у акций с Питерской биржи нет стоимости. Поэтому нам нужно её найти. Задача имеет множество решений. Я решил использовать готовый API для Yahoo! Finance. Этот API не является официальным, и автор не раз повторяет, что использование в коммерческих целях не желательно и может караться самим Yahoo. Данный API самое просто и бесплатное, что я смог найти. Есть много других подобных API, но у них, в бесплатных версиях, имеется много всяких ограничений.
Получение данных об иностранной акции
def _get_current_price(self, sec_id_choice: str) -> dict:
"""
Получение текущей цены актива с СПБ
Parameters:
sec_id_choice(str): Тикер актива для поиска
Returns:
info(dict): Словарь с ключевыми значениями актива:
stock_current_prise - стоимость в валюте
currency - валюта
rub_prise - стоимость в рублях
price_increase - изменение в цены за месяц в процентах
"""
msft = yf.Ticker(sec_id_choice)
try:
stock_current_prise = msft.info['currentPrice']
currency = msft.info['financialCurrency']
dollar_prise = self._get_currency_price(currency)
rub_prise = stock_current_prise * dollar_prise
hist = msft.history(period="1mo")
open_1mo = hist['Open'][0]
price_increase = (stock_current_prise - open_1mo) / open_1mo * 100
return {'stock_current_prise': stock_current_prise,
'currency': currency,
'rub_prise': rub_prise,
'price_increase': price_increase}
except KeyError as er:
if msft.info['regularMarketPrice'] is None:
print('f')
self.choice()
Exception(er)
При получении цены иностранного актива, мы тут же переводим из валюты в рубли. Для этого нам, само собой, надо знать в какой валюте торгуется акция и стоимость данной валюты к рублю. Информацию о валюте получаем из API для Yahoo! Finance. А стоимость валюты к рублю с сайта курса валют.
Получить стоимость валюты в рублях
@staticmethod
def _get_currency_price(currency: str) -> str:
"""
Получение текучего курса рубля относительно валюты.
Parameters:
currency(str): Валюта к которой необходимо найти курс.
Returns:
all_currencies(str): Текущая стоимость валюты в рублях
"""
all_currencies = requests.get('https://www.cbr-xml-daily.ru/daily_json.js').json()
return all_currencies['Valute'][currency]['Value']
Вывод информации
Теперь осталось красиво вывести все это на экран. Для этого есть простая функция для вывода в консоль.
Вывод в консоль
@property
def inform(self) -> str:
"""
Подготовка информации для вывода
Returns:
(str) Возвращает готовую строку с данными
"""
return f'''
сокращенно: {self.asset_choice['sec_id']}
наименование: {self.asset_choice['full_name']}
цена: {self.asset_choice['price_str']}
количество: {self.asset_choice['quantity']}
изменение за месяц: {self.asset_choice['price_increase']:.{2}f} %isn: {self.asset_choice['isn']}
'''
def print_asset(self):
"""
Функция для вывода результата с подготовленными данными в консоль
"""
print(f'''
{'-' * 60}
{self.inform}
{'-' * 28}
{self.date_create}
{'-' * 60}
''')
Так же есть метод crate_img()
который создает красивую картинку с формацией об активе, его я описывать не буду, ибо и так много текста получилось, а вывод картинки на основной функционал никак не влияет. На этом этапе можно закончить, ибо основной функционал получен.
Итоги
По итогу весь скрипт умещается в одном "неказистом" классе.
Что было реализовано:
Написан скрипт для рандомного получения активов доступных на Российском биржевом рынке
По завершению все это было запихано в Docker
Добавлена БД
В docker создан контейнер под selenium
Написан телеграмм бот
Реализован графические вывод информации об активе:
После небольшого пинка друга дизайнера, графический вывод был переделан:
А что там с деньгами?
Теперь отвечаю на второй пункт, ну тот самый который в начале, по поводу доходности. Как только я закончил писать и тестить код, большинство бумаг СПБ были запрещены для покупки "неквалам" (А за некоторые акции можно и сесть), что грустно, и по сути уничтожает половину функционала программы. Но для размышления могу дать небольшую табличку. Это акции которые были куплены, на праздно валяющиеся деньги на основном брокерском счете, во время тестирования скрипта/бота, "ради интереса", само собой по его рекомендациям. Таблица актуальна на 16.11.2022
№ |
Наименование |
дата покупки |
количество (шт) |
стоимость покупки одной акции |
прибыль в% |
---|---|---|---|---|---|
1 |
United Medical Grou |
07.10.2022 |
2 |
332,7₽ |
2,64% |
2 |
ГК Самолет |
07.10.2022 |
1 |
1975,5₽ |
32,86% |
3 |
Rosseti Tsentr PAO |
12.09.2022 |
1000 |
0,2666₽ |
3,60% |
4 |
Solid Biosciences |
12.09.2022 |
1 |
$10,04 |
-34.16% |
5 |
Tactile Systems |
07.09.2022 |
1 |
$8,36 |
0.71% |
6 |
iRobot |
07.09.2022 |
1 |
$58,89 |
-10,86% |
7 |
PetIQ |
07.09.2022 |
1 |
$10,28 |
12,93% |
8 |
Gap |
07.09.2022 |
1 |
$9,17 |
39,69% |
По итогу за пару месяцев мы имеем активы купленные примерно на 8826,15₽ (с учетом комиссии), P/L по открытию в долларах $6,42 / 4,43% и в рублях 387,84 руб. / 4,43%. Для двух месяцев результат очень даже неплохой. Но опять же, во-первых, надо понимать что больше половины акций иностранные, которые сейчас не купить, и на которые не так сильно влияют "действия" РФ. Во-вторых, покупки были не систематические и не выполняют поставленные ранее условия. В-третьих, нужно подождать хотя бы пол года (а лучше больше), что бы результат стал виден более отчетливо..
P.S: Немного боли и откровения. Статья была написана еще где-то месяц назад (сегодня 16.11.2022). Но я стал тем самым "счастливчиком" которые решил что хранить написанный текст на Хабре очень хорошая идея. Ибо в течении недели пока я писал статью, все было замечательно. А вот мой браузер видать решил что localStorage ему не нужны.... как и написанная мной статья лежавшая там. И в один "прекрасный" день я с грустью и досадой обнаружил, что кнопочки "Восстановить" больше нет, ничего нет, все пропало. Так что я начал практически с нуля, и переписывать было оочень тяжело, практически по строчки в день. А потом я столкнулся с Obsidian и обнаружил для себя, что писать статьи очень даже приятно.
Комментарии (5)
warner
22.11.2022 15:46+2Небольшое замечание по вот этому куску кода:
async def _download_stock_market_data(url, file_path): with requests.get(url) as response: if response.status_code == 200: with open(file_path, 'w') as file: json.dump(response.json(), file) async def download_data(): await asyncio.gather( _download_stock_market_data(url_ofz, config.ofz_file), _download_stock_market_data(url_bonds, config.bonds_file), _download_stock_market_data(url_bpif, config.bpif_file), _download_stock_market_data(url_shares, config.stocks_file), )
Вы делаете асинхронную функцию, но внутри используете синхронную библиотеку requests. В данном случае у вас все эти задачи внутри gather мало того, что выполняются последовательно друг за другом, так каждая из них ещё и блокирует основной EventLoop, поэтому любой медленный запрос к бирже заморозит всё, что в нём крутится. Здесь это не критично конечно, но такой подход может подложить очень болезненную ошибку при написании каких-нибудь сервисов (например запрос в упавший микросервис будет блокировать ваш собственный сервис целиком, и куча клиентов получат ошибки)
Можно взять httpx или посмотреть, какие там ещё библиотеки для async есть.wolf24ru Автор
23.11.2022 05:18+1О Код-ревью, классно! Вполне справедливое замечание. При наличии свободного времени посмотрю httpx. Возможно даже перепишу этот кусок кода.
Честно говоря, я пока что не убедился что в python есть полноценный асинхронн (и думаю не убежусь). И все же спасибо за полезное замечание.
empenoso
Идея хороша, но возможно, только для фана ????.
wolf24ru Автор
Ну так для этого, по большей части, она и делалась????. А еще для того что бы разобраться в написании бота для телеграма, и в некоторых серверных вещичках.
Кстати, а куда делась ваша статья про API МосБиржи в Google таблицах? Она на ранних стадиях помогла разобраться в некоторых нюансах API. И почему-то сейчас не отображается.
empenoso
В Т—Ж доступно.