MySQL — проблема запросов с limit и offset к очень большим таблицам и ее решение

Представим, что у нас есть таблица пользователей (users) или заказов (orders) на десятки миллионов записей и больше. И у нас стоит задача их перебора в любом контексте.

Я представляю себе это в виде цикла, в котором порционно забираем разумное количество записей, что-то вроде:

select * from users order by id limit 1000 offset 0;

Такой запрос выполняется очень быстро, моментально.

Рано или поздно мы приходим к offset, например 40 млн:

select * from users order by id limit 1000 offset 40000000;

И тут запрос может выполняться несколько минут.

Происходит это из-за того, что mysql не может гарантировать, что все элементы будут на своих порядковых местах, ведь может так статься, что элемент 40 000 099 был ранее удален и алгоритмам приходится сканировать все 40 млн элементов для получения выборки.

Решением этой проблемы может стать видоизменение запроса из limit-offset на where-limit:

 select * from users where id > 40000000 order by id limit 1000;

Выполняется моментально!

И даже, если не все элементы гарантированно присутствуют в таблице, айдишник для where легко «вытащить» из предыдущего запроса в цепочке.

Microsoft SQL Server Express Edition — автоматические бекапы базы данных

Microsoft SQL Server Express Edition — бесплатная версия Microsoft SQL Server, в которой многий функционал порезан.

Например отсутствует SQL Server Agent или Maintenance Plans, которые облегчают создание автоматических бэкапов баз данных.

В связи с этим, приходится выбирать другие пути. Один из них описан ниже.

Исходные данные для наших настроек:

  • имя базы данных: db_name
  • путь для хранения скриптов и бэкапов базы данных db_name: «d:\projects\db_backups»
  • периодичность создания бэкапов: раз в 1 день
  • срок хранения бекапов: 3 дня

Разделим скрипт бекапов на 2 шага:

  1. создание и сохранение бекапа
  2. очистка папки от старых бэкапов

Для первого шага:

a) Запускаем нижеследующий скрипт, например в Management Studio, чтобы записалась хранимая процедура на master базе данных

USE [master]
GO
/****** Object:  StoredProcedure [dbo].[sp_BackupDatabases]    Script Date: 8/12/2021 6:26:34 PM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================  
-- Author: Microsoft  
-- Create date: 2010-02-06 
-- Description: Backup Databases for SQLExpress 
-- Parameter1: databaseName  
-- Parameter2: backupType F=full, D=differential, L=log 
-- Parameter3: backup file location 
-- ============================================= 
CREATE PROCEDURE [dbo].[sp_BackupDatabases]   
            @databaseName sysname = null, 
            @backupType CHAR(1), 
            @backupLocation nvarchar(200)  
AS  
       SET NOCOUNT ON;  
            DECLARE @DBs TABLE 
            ( 
                  ID int IDENTITY PRIMARY KEY, 
                  DBNAME nvarchar(500) 
            ) 
             -- Pick out only databases which are online in case ALL databases are chosen to be backed up 
             -- If specific database is chosen to be backed up only pick that out from @DBs 
            INSERT INTO @DBs (DBNAME) 
            SELECT Name FROM master.sys.databases 
            where state=0 
            AND name= ISNULL(@databaseName ,name)
            ORDER BY Name
            -- Filter out databases which do not need to backed up 
            IF @backupType='F' 
                  BEGIN 
                  DELETE @DBs where DBNAME IN ('tempdb','Northwind','pubs','AdventureWorks') 
                  END 
            ELSE IF @backupType='D' 
                  BEGIN 
                  DELETE @DBs where DBNAME IN ('tempdb','Northwind','pubs','master','AdventureWorks') 
                  END 
            ELSE IF @backupType='L' 
                  BEGIN 
                  DELETE @DBs where DBNAME IN ('tempdb','Northwind','pubs','master','AdventureWorks') 
                  END 
            ELSE 
                  BEGIN 
                  RETURN 
                  END 
            -- Declare variables 
            DECLARE @BackupName nvarchar(100) 
            DECLARE @BackupFile nvarchar(300) 
            DECLARE @DBNAME nvarchar(300) 
            DECLARE @sqlCommand NVARCHAR(1000)  
	        DECLARE @dateTime NVARCHAR(20) 
            DECLARE @Loop int                   
            -- Loop through the databases one by one 
            SELECT @Loop = min(ID) FROM @DBs 
      WHILE @Loop IS NOT NULL 
      BEGIN 
-- Database Names have to be in [dbname] format since some have - or _ in their name 
      SET @DBNAME = '['+(SELECT DBNAME FROM @DBs WHERE ID = @Loop)+']' 
-- Set the current date and time n yyyyhhmmss format 
      SET @dateTime = REPLACE(CONVERT(VARCHAR, GETDATE(),101),'/','') + '_' +  REPLACE(CONVERT(VARCHAR, GETDATE(),108),':','')   
-- Create backup filename in path\filename.extension format for full,diff and log backups 
      IF @backupType = 'F' 
            SET @BackupFile = @backupLocation+REPLACE(REPLACE(@DBNAME, '[',''),']','')+ '_FULL_'+ @dateTime+ '.BAK' 
      ELSE IF @backupType = 'D' 
            SET @BackupFile = @backupLocation+REPLACE(REPLACE(@DBNAME, '[',''),']','')+ '_DIFF_'+ @dateTime+ '.BAK' 
      ELSE IF @backupType = 'L' 
            SET @BackupFile = @backupLocation+REPLACE(REPLACE(@DBNAME, '[',''),']','')+ '_LOG_'+ @dateTime+ '.TRN' 
-- Provide the backup a name for storing in the media 
      IF @backupType = 'F' 
            SET @BackupName = REPLACE(REPLACE(@DBNAME,'[',''),']','') +' full backup for '+ @dateTime 
      IF @backupType = 'D' 
            SET @BackupName = REPLACE(REPLACE(@DBNAME,'[',''),']','') +' differential backup for '+ @dateTime 
      IF @backupType = 'L' 
            SET @BackupName = REPLACE(REPLACE(@DBNAME,'[',''),']','') +' log backup for '+ @dateTime 
-- Generate the dynamic SQL command to be executed 
       IF @backupType = 'F'  
                  BEGIN 
               SET @sqlCommand = 'BACKUP DATABASE ' +@DBNAME+  ' TO DISK = '''+@BackupFile+ ''' WITH INIT, NAME= ''' +@BackupName+''', NOSKIP, NOFORMAT' 
                  END 
       IF @backupType = 'D' 
                  BEGIN 
               SET @sqlCommand = 'BACKUP DATABASE ' +@DBNAME+  ' TO DISK = '''+@BackupFile+ ''' WITH DIFFERENTIAL, INIT, NAME= ''' +@BackupName+''', NOSKIP, NOFORMAT'         
                  END 
       IF @backupType = 'L'  
                  BEGIN 
               SET @sqlCommand = 'BACKUP LOG ' +@DBNAME+  ' TO DISK = '''+@BackupFile+ ''' WITH INIT, NAME= ''' +@BackupName+''', NOSKIP, NOFORMAT'         
                  END 
-- Execute the generated SQL command 
       EXEC(@sqlCommand) 
-- Goto the next database 
SELECT @Loop = min(ID) FROM @DBs where ID>@Loop 
END 

b) В папке бэкапов создаем исполняемый файл с расширение bat (например, sql_db_backup.bat) и содержимым

sqlcmd -S .\SQLEXPRESS -E -Q "EXEC sp_BackupDatabases @backupLocation='d:\projects\db_backups', @databaseName='db_name', @backupType='F'"

где ‘d:\projects\db_backups‘ — здесь будем хранить бэкапы, db_name — имя базы данных (из условий выше)

с) создаем в Расписании Виндовс (Task Scheduler) задачу для выполнения скрипта c параметрами

  • Create Basic Task…
  • Run whether user is logged on or not’ выбранный, затем выбираем ‘Do not store password…
  • Trigger: Daily, и время, когда будет выполняться скрипт
  • Actions: Start a program — и указываем путь к bat файлу, созданному ранее — sql_db_backup.bat
  • Остальные параметры второстепенны

Уже на этом шаге у нас будут создаваться ежедневные бэкапы, но мы пойдем дальше и будем очищать старые бекап, чтобы освобождать место на жестов диске.

Для второго шага:

a) создадим скрипт с расширением bat (например, sql_clean_backups.bat) и содержимым

forfiles -p d:\projects\db_backups\ -m *.bak* /D -3 /C "cmd /c del /q @path"

который будет удалять файлы с расширением .bak старше 3х дней

b) создаем в Расписании Виндовс (Task Scheduler) задачу для выполнения скрипта c параметрами аналогичными пункту (c) первого шага.

На этом все…

PHP foreach — как легко переполнить память даже с крошечным массивом

PHP foreach пример

Рассмотрим простой пример перебора элементов одномерного массива с 4мя элементами

<?php
$elements = [1,2,3,4];
echo PHP_EOL;
foreach ($elements as $element) {
    echo $element;
}
echo PHP_EOL;
echo json_encode($elements);

при выполнении все ожидаемо:

1234
[1,2,3,4]

Теперь, перейдем к какой то задаче с пользой, например, если встречается какой то элемент, необходимо добавить его копию в исходный массив

<?php
$elements = [1,2,3,4];
echo PHP_EOL;
foreach ($elements as $element) {
    if ($element === 3) {
        $copy = $element;
        $elements[] = $copy;
    }
    echo $element;
}
echo PHP_EOL;
echo json_encode($elements);

В результате получаем:

1234
[1,2,3,4,3]

А теперь, рассмотрим вполне реальную ситуацию, когда в цикле с элементами массива нужно что то сделать и сохранить это:

<?php
$elements = [1,2,3,4];
echo PHP_EOL;
foreach ($elements as &$element) {
    // do something with every element
    if ($element === 3) {
        $copy = $element;
        $elements[] = $copy;
    }
    echo $element;
}
echo PHP_EOL;
echo json_encode($elements);

И вот тут мы сталкиваемся с проблемой похлеще рекурсии без предусмотренного выхода из нее:

123.....333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333
PHP Fatal error:  Allowed memory size of 134217728 bytes exhausted (tried to allocate 134217736 bytes) in /php on line 7
PHP Stack trace:
PHP   1. {main}() /php:0

Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 134217736 bytes) in /php on line 7

Call Stack:
    0.0025     392288   1. {main}() /php:0

Это последствия передачи массива по ссылке

И лучше обойти это с помощью временного массива:

<?php
$elements = [1,2,3,4];
echo PHP_EOL;
$additionalElements = [];
foreach ($elements as &$element) {
    // do something with every element
    if ($element === 3) {
        $additionalElements[] = $element;
    }
    echo $element;
}
echo PHP_EOL;
echo json_encode(array_merge($elements, $additionalElements));

И, необходимый нам результат:

1234
[1,2,3,4,3]

Инвестиционный дивидендный калькулятор на Python

В этой статье, я опишу как создавал инвестиционный и дивидендный калькулятор который наращивает ваш капитал по экспоненте за счет сложного процесса.

Ниже приведено несколько скриншотов

Реальный работающий пример на хостинге можно найти по адресу

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

  1. Входной точкой проекта будет index.py, роль которого — общая разметка страницы и роутинг (сможем задавать урлы будущим веб-страницам)
  2. Приложение Dash будет инициализироваться в app.py
  3. В папке apps будем хранить содержимое и логику отдельных страниц. Далее внутри этой папки:
  4. __init__.py — в нашем случае, необязательный системный файл
  5. commonmodules.py — общая разметка страниц, верхнее горизонтальное меню, которое поможет навигировать между нашими страницами — домашней страницей и страницей дивидендного калькулятора
  6. home.py — домашняя страница с простейшей версткой
  7. 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 лет и больше, даже, пугают
  • другие выводы можете сделать самостоятельно, «поиграв» с настройками, входными параметрами

Подготовка, публикация и сео-оптимизация статьи WordPress

Попадая в административную панель блога на вордпресс мы сразу же хотим создать свою первую статью. Я решил попробовать с приветственного слова.

Но, сперва, стоит внести простейшие изменения, чтобы ваш блог с первой статьей не был «Очередной сайт на WordPress»

Переходим в Настройки и вводим Заголовок вот какой нам понравится, и заполняем другие поля актуальными данными и сохраняем.

В разделе Страниц нам необходимо или же отредактировать единственную там опубликованные страницу (обычно там размещается информация об авторе блога или его компании) или же удалить ее.

Затем, в разделе Записи, выбираем первую статью, сгенерированную системой, нажимаем Изменить и описываем, например, что сподвигло вас начать вести этот блог. Сохраняем изменения и публикуем статью (совет: чаще сохранять изменения). Любуемся результатами.

По большему счету это все, если вы ведете блог только для себя или своей души. В противном же случае, желательно, провести, хотя бы минимальные работы по сео-оптимизации.

Рекомендую поставить plugin All In One SEO Pack, один из «топовых» расширений для WordPress по данной тематике (весь необходимый нам функционал размещен в бесплатной версии). Итак, заходим в раздел Плагины, нажимаем на кнопку Добавить новый и в строке поиска вводим «All In One SEO Pack»

Искомый плагин будет первым в списке, и в его карточке нажимаем кнопку Установить.

После этого, возвращаемся в раздел Плагины и для нашего плагина нажимает Активировать для его «включения».

Перейдем к общим настройкам SEO — там же нажимаем, соответсвенно, Настройки SEO.

Большинство настроек уже хорошо смотрятся со значениями по умолчанию. Стоит обратить внимание на следующие, отредактировав их:

Заголовок дом. страницы — указываем краткий, емкий заголовок для вашего блога

Описание дом. страницы — указываем чуть более развернутое описание

Google Analytics ID — да-да, тут необходимо создать счетчик для гугл аналитики. Делается это несложно — процесс описан в одной из следующих статей (ссылка)

Исключить отслеживание пользователей — чтобы не влиять на статистику по сайту нашими проверками веб-страниц в блоге

После этих немногочисленных изменений в работе плагина, в самом низу списка, нажимаем кнопку Обновить настройки.

А, теперь, мы можем за-апгрейдить нашу первую статью-запись.

Возвращаемся в раздел редактирования Записей

При наведении на название статьи появляются элементы управления — выбираем Изменить.

Справа от самой Записи есть элементы управления. Из значимых для нас:

Постоянная ссылка — так как строка урла браузера больше дружит с латиницей (последствия систем кодировок), советую адрес ввести латинскими буквами, произведя транслитерацию в случае русского-буквенного предлагаемого урла

Рубрики — тоже советую этот инструмент, категоризируя свои статьи, например «administration», «code», «books», «uncategorized», и другие

Метки — указываем ключевые тэги (это так же помогаем категоризировать наши статьи, но в другом срезе, а, кроме того, создает новые страницы в нашем блоге — страницы со статьями по определенному тегу, что положительно влияет на сео)

Последний важный момент — скролим в самый низ нашей статьи до раздела All In One SEO Pack и заполняем поля:

Заголовок — указываем краткий, но емкий заголовок для это статьи

Описание — указываем более развернутое описание

Чуть выше отображается «сниппет» — как будет выглядеть интерпритация страницы статьи в поисковиках.

Последний раз сохраняем и нажимаем кнопку Обновить, чтобы опубликовать изменения.