В этой статье, я опишу как создавал инвестиционный и дивидендный калькулятор который наращивает ваш капитал по экспоненте за счет сложного процесса.
Ниже приведено несколько скриншотов
Реальный работающий пример на хостинге можно найти по адресу
https://invest.questpro.club/dividends-calculator
Калькулятор хостится на Digital Ocean, сервер на Centos 7, Python + Dash фреймворк для визуализации.
С данным реальным примером, можно начать увлекательное путешествие в мир финансовой грамотности, попутно, ознакомившись с языком Питон, который идеально подходит под задачи автоматизации математических вычислений, а так же фреймворком Даш (на основе более известного фреймворка Flask c элементами ReactJS, но это все под капотом)
Принцип работ инвестиций в дивидендные акции
Один из самых интересных и надежных способов получать пассивный доход — это инвестиции в надежные дивидендные акции.
Наиболее оправданный подход (проверенный годами и многими публичными пассивными инвесторами) — это вкладывать регулярно (например раз в месяц или раз в неделю) одинаковыми суммами в понятные и надежные компании, которые годами наращивают свою выручку и, соответсвенно, дивиденды.
Далее, эти растущие дивиденды ре-инвестируются, что увеличивает регулярные пополнения. Машина раскручивается, и капитал растет.
Все это происходит по экспоненте, как можно убедиться после использования данного калькулятора.
Установка окружения для нашего проекта и размещение на хостинге, публикация.
Подразумевается, что хостинг приобретен, операционная система установлена, веб сервер настроен (или же вы работаете на локальном компьютере на Mac или Linux)
Устанавливаем Python, Dash
> sudo su # yum -y install python3 # python3 —version # yum -y install python3-devel
Ранее была создана директория под наш проект (/var/www/invests/). Переходим к ней
# cd /var/www/invests/
Устанавливаем виртуальную среду, для изоляции нашего проекта и дальнейшего открытия его всему интернету
# python3 -m venv venv # cd /var/www/invests/venv/bin # source activate
Устанавливаем Flask и Gunicorn, как наиболее распространенный вариант надстройки над веб-сервером, для приложений на Питоне
# pip3 install gunicorn flask
И наш фреймворк для визуализации Даш
# pip3 install dash==1.12.0
Далее можно тестировать конфигурацию, прежде чем разворачивать проект. Создадим 2 тестовых файла в директории проекта — app.py
# cd /var/www/invests # nano app.py
Вставляем код ниже
#from flask import Flask import dash app = dash.Dash()
Сохраняем CTRL+O и закрываем редактор CRTL+X
и наш входной файл — index.py
# nano index.py
Вставляем код ниже
import dash_core_components as dcc import dash_html_components as html from dash.dependencies import Input, Output from app import app app.layout = html.Div([ html.Label(children="Hello world"), ]) server = app.server app.config.suppress_callback_exceptions = True if __name__ == '__main__': app.run_server(debug=True)
Сохраняем CTRL+O и закрываем редактор CRTL+X
Теперь мы готовы запускать нашу тестовую среду (более подробную документацию на английском языке, если это понадобится, можно найти здесь)
# gunicorn --bind 0.0.0.0:8000 index:server
Наше страничка «Hello world» доступна по адресу
http://{ip_адрес_сервера}:8000
Если успешно запустили, дезактивируем виртуальную среду
# deactivate
и приступаем к настройкам веб-сервера (коим в нашем примере небезызвестный Caddy), чтобы калькулятор был доступен по доменному имени.
Первым делом создаем .service файл в папке /etc/systemd/system
# nano /etc/systemd/system/invests.service
и вставляем следующий код
[Unit] Description=Gunicorn instance to serve invests After=network.target [Service] User=caddy Group=caddy WorkingDirectory=/var/www/invests Environment="PATH=/var/www/invests/venv/bin" ExecStart=/var/www/invests/venv/bin/gunicorn --workers 3 --bind unix:invests.sock -m 007 index:server [Install] WantedBy=multi-user.target
Сохраняем CTRL+O и закрываем редактор CRTL+X
Таким образом мы можем стартовать наш сервис (и добавить в авто-загрузку)
# systemctl start invests # systemctl enable invests
И, последним делом, осталось дать понять, где искать наш сервис веб-серверу. В случае с Caddy это делается максимально просто, плюс с коробки получаем сертификат
# nano /etc/caddy/Caddyfile
и добавляем следующую конфигурацию
your-domain.com { tls admin@your-domain.com proxy / unix:/var/www/invests/invests.sock }
Вместо your-domain.com указываем ваше доменное имя
Сохраняем CTRL+O и закрываем редактор CRTL+X
Перезагружаем веб-сервер
# systemctl restart caddy.service
После этого наша тестовая страничка будет доступна по адресу домена
https://your-domain.com
Структура проекта
Для дивидендного калькулятора будем использовать лаконичную структуру файлов, представленную ниже
Путь к проекту: /var/www/invests
- Входной точкой проекта будет index.py, роль которого — общая разметка страницы и роутинг (сможем задавать урлы будущим веб-страницам)
- Приложение Dash будет инициализироваться в app.py
- В папке apps будем хранить содержимое и логику отдельных страниц. Далее внутри этой папки:
- __init__.py — в нашем случае, необязательный системный файл
- commonmodules.py — общая разметка страниц, верхнее горизонтальное меню, которое поможет навигировать между нашими страницами — домашней страницей и страницей дивидендного калькулятора
- home.py — домашняя страница с простейшей версткой
- dividends_calculator.py — собственно, страница дивидендного калькулятора, для чего мы все это и проделывали
В следующей разделе приступим к наполнению всех этих страниц
Код проекта
так же доступен в git
# cd /var/www/invests # echo ‘’ > index.py && nano index.py
Вставляем код ниже и сохраняем
import dash_core_components as dcc import dash_html_components as html from dash.dependencies import Input, Output from app import app from apps import home, dividends_calculator app.layout = html.Div([ dcc.Location(id='url', refresh=False), html.Div(id='page-content') ]) server = app.server app.config.suppress_callback_exceptions = True @app.callback(Output('page-content', 'children'), [Input('url', 'pathname')]) def display_page(pathname): if pathname == '/': return home.layout elif pathname == '/dividends-calculator': return dividends_calculator.layout else: return '404' if __name__ == '__main__': app.run_server(debug=True)
# nano app.py
Вставляем код ниже и сохраняем
#from flask import Flask import dash import dash_core_components as dcc import dash_html_components as html print(dcc.__version__) # 0.6.0 or above is required external_css = ["https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css", "https://cdnjs.cloudflare.com/ajax/libs/normalize/7.0.0/normalize.min.css", "https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css", "https://fonts.googleapis.com/css?family=Raleway:400,300,600", "https://codepen.io/chriddyp/pen/bWLwgP.css", "https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"] app = dash.Dash(external_stylesheets=external_css)
# mkdirs apps && cd apps
# nano __init__.py
Оставляем его пустым и сохраняем.
Далее
# nano commonmodules.py
Вставляем код ниже и сохраняем
import dash_core_components as dcc import dash_html_components as html from dash.dependencies import Input, Output def get_header(): header = html.Div([ html.Div([ html.H1( 'List of Dashes') ], className="twelve columns padded"), ], className="gs-text-header") return header def get_menu(): menu = html.Div([ dcc.Link('Home ', href='/', className="p-2 text-dark"), dcc.Link('Dividends Calculator ', href='/dividends-calculator', className="p-2 text-dark") ], className="d-flex flex-column flex-md-row align-items-center p-3 px-md-4 mb-3 bg-white border-bottom shadow-sm") return menu
# nano home.py
Вставляем код ниже и сохраняем
import dash_core_components as dcc import dash_html_components as html from dash.dependencies import Input, Output from apps import commonmodules from app import app layout = html.Div([ commonmodules.get_menu(), html.H1('This is home screen'), html.A('My blog', href='https://questpro.club') ])
# nano dividends_calculator.py
Вставляем код ниже и сохраняем
# -*- coding: utf-8 -*- import dash_core_components as dcc import dash_html_components as html from dash.dependencies import Input, Output from apps import commonmodules from app import app meta_tags = [ {'name':'description', 'content':'Дивидендный калькулятор с учетом сложного процента'}, {'name':'title', 'content':'Дивидендный калькулятор'} ] #app.external_stylesheets = external_stylesheets app.meta_tags = meta_tags app.title = 'Дивидендный калькулятор' bottom_text = ''' 1) Все суммы в долларах, проценты - в % 2) По умолчанию, средний размер дивидентов при покупке акций устанавливаем в 4%, можно поменять 3) По умолчанию, средний рост цены акций в год устанавливаем в 12.5% на основе статистики по S&P Dividends aristocrats 4) Все дивиденды реинвестируем для осуществления скорейшего роста - сложный процент ''' default__divident_income_per_month = 1000 default__start_capital = 1000 default__regular_payment = 1000 default__start_divident_percent = 4 default__average_cost_grow_percent = 12.5 layout = html.Div([ commonmodules.get_menu(), html.H1('Дивидендный калькулятор с учетом сложного процента'), html.Div([ html.Div([ html.Div([], className="col-sm-1"), html.Label(children='Желаемый средний доход в месяц по дивидентам ($)', className="col-sm-4 col-form-label"), html.Div([ dcc.Input(id='divident-income-per-month', value='1000', type='text', className="form-control-plaintext") ], className="col-sm-4") ], className="form-group row"), html.Div([ html.Div([], className="col-sm-1"), html.Label(children='Первоначальный взнос ($)', className="col-sm-4 col-form-label"), html.Div([ dcc.Input(id='start-capital', value='1000', type='text', className="form-control-plaintext") ], className="col-sm-4") ], className="form-group row"), html.Div([ html.Div([], className="col-sm-1"), html.Label(children='Частота очередного поступления: 1 месяц', className="col-sm-4 col-form-label") ], className="form-group row"), html.Div([ html.Div([], className="col-sm-1"), html.Label(children='Размер очередного поступления ($)', className="col-sm-4 col-form-label"), html.Div([ dcc.Input(id='regular-payment', value='1000', type='text', className="form-control-plaintext") ], className="col-sm-4") ], className="form-group row"), html.Div([ html.Div([], className="col-sm-1"), html.Label(children='Средний размер дивидентов в при покупке акций (%)', className="col-sm-4 col-form-label"), html.Div([ dcc.Input(id='start-divident-percent', value='4', type='text', className="form-control-plaintext") ], className="col-sm-4") ], className="form-group row"), html.Div([ html.Div([], className="col-sm-1"), html.Label(children='Средний рост цены акций в год (%)', className="col-sm-4 col-form-label"), html.Div([ dcc.Input(id='average-cost-grow-percent', value='12.5', type='text', className="form-control-plaintext") ], className="col-sm-4") ], className="form-group row"), html.Div([ html.Div([], className="col-sm-1"), html.Label(children='Отображаем на графиках максимум лет', className="col-sm-4 col-form-label"), ], className="form-group row"), dcc.Slider( id='maximum-years', min=10, max=50, marks={i: 'рассматриваем максимум лет {}'.format(i) if i == 1 else str(i) for i in range(5, 51)}, value=11 ), html.Div([ html.Div([], className="col-sm-1"), html.Label(id='result', className="col-sm-8 col-form-label"), ], className="form-group row") ], className=""), html.Div([ dcc.Graph(id='dividends-graph'), dcc.Graph(id='capital-graph') ]), html.Div([ dcc.Markdown(children=bottom_text) ]) ]) def create_dividends_graph(df, target, title): return { 'data': [dict( x=df['month'], y=df['dividend_no_reinv'], mode='lines', name='Дивиденды - без реинвестиций и без роста цены акций' ), dict( x=df['month'], y=df['dividend_percent_reinv'], mode='lines', name='Дивиденды - рост за счет роста акций' ), dict( x=df['month'], y=df['dividend_all_reinv'], mode='lines', name='Дивиденды - рост цен акций и реинвестирование дивидендов' ), dict( x=[target['month'],], y=[target['dividend'],], mode='marks', name='Целевое значение дивидендов' )], 'layout': { 'annotations': [{ 'x': 0, 'y': 0, 'xanchor': 'left', 'yanchor': 'bottom', 'xref': 'paper', 'yref': 'paper', 'showarrow': False, 'align': 'left', 'bgcolor': 'rgba(255, 255, 255, 0.5)', 'text': title }], 'yaxis': {'type': 'linear', 'title': 'Дивиденды в месяц, $'}, 'xaxis': {'showgrid': True, 'title': 'Месяцы'} } } def create_capital_graph(df, target, title): return { 'data': [dict( x=df['month'], y=df['cap_no_reinv'], mode='lines', name='Капитал - вложенные деньги' ), dict( x=df['month'], y=df['cap_percent_reinv'], mode='lines', name='Капитал - с ростом цены акций' ), dict( x=df['month'], y=df['cap_all_reinv'], mode='lines', name='Капитал - с ростом цены акций и реинвестицией дивидендов' )], 'layout': { 'annotations': [{ 'x': 0, 'y': 0, 'xanchor': 'left', 'yanchor': 'bottom', 'xref': 'paper', 'yref': 'paper', 'showarrow': False, 'align': 'left', 'bgcolor': 'rgba(255, 255, 255, 0.5)', 'text': title }], 'yaxis': {'type': 'linear', 'title': 'Капитал, $'}, 'xaxis': {'showgrid': True, 'title': 'Месяцы'} } } def total_result_div(result): if result['dividend'] <= 0: return "Желаемая дивидендная доходность не найдена в заданном промежутке времени, попробуйте выставить больше лет для графиков" return "Требуемый ежемесячный дивидендный результат будет достигнут через [ {} месяцев], при этом будет вложено: [ {}$ ], но ваш капитал достигнет к этому времени за счет сложного процента: [ {:.2f}$ ]".format(result['month'], result['current_capital_no_reinv'], result['current_capital_with_all_reinv']) """ Var in input field might be not floar, but empty or string, in such case use default value """ def input_to_float(var, default): if var.isdigit(): return float(var) else: return default @app.callback( [Output(component_id='dividends-graph',component_property='figure'), Output(component_id='capital-graph',component_property='figure'), Output(component_id='result', component_property='children')], [Input(component_id='maximum-years', component_property='value'), Input(component_id='divident-income-per-month', component_property='value'), Input(component_id='start-capital', component_property='value'), Input(component_id='regular-payment', component_property='value'), Input(component_id='start-divident-percent', component_property='value'), Input(component_id='average-cost-grow-percent', component_property='value')] ) def update_output_div(maximum_years, divident_income_per_month, start_capital, regular_payment, start_divident_percent, average_cost_grow_percent): max_years_number = int(maximum_years) MONTH_COUNT_YEAR = 12 divident_income_per_month = input_to_float(divident_income_per_month, default__divident_income_per_month) start_capital = input_to_float(start_capital, default__start_capital) regular_payment = input_to_float(regular_payment, default__regular_payment) start_divident_percent = input_to_float(start_divident_percent, default__start_divident_percent) average_cost_grow_percent = input_to_float(average_cost_grow_percent, default__average_cost_grow_percent) current_capital_no_reinv = current_capital_just_percent_reinv = current_capital_with_all_reinv = start_capital info = {} info['month'] = [] info['dividend_no_reinv'] = [] info['dividend_percent_reinv'] = [] info['dividend_all_reinv'] = [] info['cap_no_reinv'] = [] info['cap_percent_reinv'] = [] info['cap_all_reinv'] = [] target = {'month': 0, 'dividend': 0, 'current_capital_no_reinv': 0, 'current_capital_with_all_reinv': 0} for month in range(max_years_number * MONTH_COUNT_YEAR): info['month'].append(month) current_capital_no_reinv += regular_payment info['cap_no_reinv'].append(current_capital_no_reinv) current_dividend_no_reinv = current_capital_no_reinv * start_divident_percent / 100 / MONTH_COUNT_YEAR info['dividend_no_reinv'].append(current_dividend_no_reinv) current_capital_just_percent_reinv = (current_capital_just_percent_reinv + regular_payment) * (1 + average_cost_grow_percent / 100 / MONTH_COUNT_YEAR) info['cap_percent_reinv'].append(current_capital_just_percent_reinv) current_dividend_just_percent_reinv = current_capital_just_percent_reinv * start_divident_percent / 100 / MONTH_COUNT_YEAR info['dividend_percent_reinv'].append(current_dividend_just_percent_reinv) current_dividend_with_all_reinv = current_capital_with_all_reinv * start_divident_percent / 100 / MONTH_COUNT_YEAR info['dividend_all_reinv'].append(current_dividend_with_all_reinv) current_capital_with_all_reinv = (current_capital_with_all_reinv + regular_payment + current_dividend_with_all_reinv) * (1 + average_cost_grow_percent / 100 / MONTH_COUNT_YEAR) info['cap_all_reinv'].append(current_capital_with_all_reinv) if((current_dividend_with_all_reinv >= divident_income_per_month) and not (target['month'])): target['month'] = month target['dividend'] = current_dividend_with_all_reinv target['current_capital_no_reinv'] = current_capital_no_reinv target['current_capital_with_all_reinv'] = current_capital_with_all_reinv return create_dividends_graph(info, target, ''), create_capital_graph(info, target, ''), total_result_div(target)
Перезапускаем сервис, чтобы применить все изменения на веб сайте
# systemctl restart invests
Работа дивидендного калькулятора
Значения по-умолчанию для основных входных данных:
- желаемый ежемесячный доход от дивидендов — 1000 долларов
- стартовый капитал — 1000 долларов
- ежемесячные пополнения брокерского счета для покупки дивидендных акций — 1000 долларов
- средний процент дивидендов по акциям — 4%
- средний рост цены дивидендных акций — 12.5% (на основе статистики по S&P Dividends aristocrats)
Все входные данные размещаются в редактируемых полях
Результаты можно увидеть на графиках ниже, куда поступают данные после обработки
- график роста ежемесячных дивидендов:
a) получаемый за счет роста цены акций и ежемесячных ре-инвестирований дивидендов,
б) получаемый только за счет роста цены акций, т.е. если дивиденды будем выводить (без ре-инвестирования),
в) получаемый, если бы мы просто клали деньги «под подушку», без инвестирования, вообще
- график роста капитала при тех же трех, рассматриваемых в первом пункте, условиях (а), б), в))
Бонусом, над графиками, представлен «ползунок» взгляда в будущее (изменяется в месяцах): что же произойдет с нашими дивидендами и капиталом, если продолжать вкладывать деньги следуя той же стратегии и далее
Итог
При взгляде на 2 графика: роста дивидендного дохода и роста капитала, можно сделать несколько интересных выводов:
- значительно выгоднее вкладывать деньги в хорошие компании, чем просто хранить их
- значительно выгоднее ре-инвестировать дивиденды
- особенно, эффект заметен на горизонте более 10 лет
- рост происходит экспоненциально
- цифры по прошествии 20 лет и больше, даже, пугают
- другие выводы можете сделать самостоятельно, «поиграв» с настройками, входными параметрами