В этой статье, я опишу как создавал инвестиционный и дивидендный калькулятор который наращивает ваш капитал по экспоненте за счет сложного процесса.
Ниже приведено несколько скриншотов
Реальный работающий пример на хостинге можно найти по адресу
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 лет и больше, даже, пугают
- другие выводы можете сделать самостоятельно, «поиграв» с настройками, входными параметрами
Едва могу тому верить.
Hi!
This is my first visit to your blog! We are a group of volunteers and starting a new
initiative in a community in the same
niche. Your blog provided us valuable information
to work on. You have done a marvellous job!
Asking questions are actually nice thing if
you are not understanding something entirely, but this piece of
writing gives pleasant
understanding yet.
I’d like to thank you for the efforts you’ve put in writing this blog.
I really hope to check out the same high-grade blog
posts by you later on as well. In truth, your creative writing abilities has
encouraged me to get my very own site now 😉
Дивидендный калькулятор на Python
Дивидендный калькулятор на Python | Questpro Club
Greetings! I’ve been reading your site for a long time now and finally got the bravery to go ahead and give you a shout out from Humble Texas! Just wanted to tell you keep up the great work!