Анализируя криптовалютные рынки с помощью Python

в 7:50, , рубрики: data science, guides, python, Wirex, Блог компании Wirex, код, Криптовалюты, Программирование, финансы

Как ведут себя Биткоин-рынки? Каковы причины внезапных взлетов и падений цен на криптовалюты? Есть ли между рынками альткоинов тесная неразделимая связь или же они по большей части не зависят друг от друга? Как можно предсказать, что произойдет в дальнейшем?

image

Информационно-аналитический подход к криптовалютным рассуждениям

Посвященные криптовалютам вроде Биткоина и Ethereum статьи изобилуют рассуждениями и теориями. Сотни самопровозглашенных экспертов приводят аргументы в пользу трендов, которые, по их мнению, проявят себя в скором времени. Чего точно не хватает многим подобным анализам, так это прочного фундамента в виде данных и статистики, способных поддержать те или иные утверждения.

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

image

Этот пост не будет посвящен объяснению того, что такое криптовалюты (если Вам нужно такое объяснение я бы порекомендовал Вам вот этот отличный обзор). Не будет здесь и рассуждений по поводу того, какие конкретные валюты вырастут или упадут в цене. Вместо этого, руководство будет посвящено получению доступа к грубым, необработанным данным и поиску истории, скрытых под пластами чисел.

Этап 1. Обустраиваем нашу лабораторию

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

Полная версия проделанной работы и все ее результаты доступна здесь.

1.1 Устанавливаем Anaconda

Самый простой способ установки зависимостей с нуля для этого проекта — использование Anaconda — python-экосистемы и менеджера зависимостей, содержащего все необходимые пакеты для работы с данными и их анализа.

Для установки Anaconda я бы порекомендовал воспользоваться официальной инструкцией, доступной здесь.

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

1.2 Настройка окружения проекта в Anaconda

Как только Anaconda будет установлена, мы захотим создать новую среду для упорядочивания работы с зависимостями.

Введите команду conda create --name cryptocurrency-analysis python=3 для создания нового окружения Anaconda для нашего проекта.

Далее вводим source activate cryptocurrency-analysis и (на Linux/macOS) или activate cryptocurrency-analysis (на Windows) для активации среды.

И, наконец, команда conda install numpy pandas nb_conda jupyter plotly quandl установит в среде необходимые зависимости. Это процесс может занять несколько минут.

Почему мы пользуемся окружением? Если вы планируете одновременную работу со множеством Python-проектов на своем компьютере, полезно размещать зависимости (программные библиотеки и пакеты) отдельно во избежание конфликтов. В рамках каждого проекта Anaconda создает в среде специальный каталог для зависимостей, что позволяет отделить их от зависимостей других проектов и упорядочить работу с ними.

1.3 Запуск интерактивной тетради Jupyter Notebook

Как только среда и зависимости будут установлены, введите в консоли jupyter notebook для запуска ядра iPython и откройте в браузере ссылку http://localhost:8888/. Создайте новую Python-тетрадь, проверив, что для нее используется ядро Python [conda env:cryptocurrency-analysis].

image

1.4 Импорт зависимостей наверх тетради

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

import os
import numpy as np
import pandas as pd
import pickle
import quandl
from datetime import datetime

Кроме, того надо импортировать Plotly и включить для него оффлайн-режим.

import plotly.offline as py
import plotly.graph_objs as go
import plotly.figure_factory as ff
py.init_notebook_mode(connected=True)

Этап 2. Получение ценовых данных Биткоина

Теперь, когда все настройки завершены, мы готовы приступить к получению информации для анализа. Прежде всего нам потребуется запросить ценовые данные Биткоина с помощью бесплатного биткоин-API Quandl.

2.1 Задаем вспомогательную функцию Quandl

Для помощи с получением данных мы зададим функцию, скачивающую и кэширующую наборы данных из Quandl.

def get_quandl_data(quandl_id):
    '''Download and cache Quandl dataseries'''
    cache_path = '{}.pkl'.format(quandl_id).replace('/','-')
    try:
        f = open(cache_path, 'rb')
        df = pickle.load(f)   
        print('Loaded {} from cache'.format(quandl_id))
    except (OSError, IOError) as e:
        print('Downloading {} from Quandl'.format(quandl_id))
        df = quandl.get(quandl_id, returns="pandas")
        df.to_pickle(cache_path)
        print('Cached {} at {}'.format(quandl_id, cache_path))
    return df

Для преобразования скачанных данных и сохранения их в файл мы воспользуемся pickle. Это позволит предотвратить повторное скачивание одних и тех же данных каждый раз, когда мы запускаем скрипт. Функция будет возвращать данные в виде фрейма данных Pandas. Если вы незнакомы с фреймами данных, можете представлять их в виде очень мощных электронных таблиц.

2.2 Берем ценовые данные с биржи Kraken

Для начала давайте подтянем исторические данные по курсу биткоина с биржи Kraken.

# Pull Kraken BTC price exchange data
btc_usd_price_kraken = get_quandl_data('BCHARTS/KRAKENUSD')

Мы можем проверить первые 5 строк фрейма данных с помощью метода head().

btc_usd_price_kraken.head()

image

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

# Chart the BTC pricing data
btc_trace = go.Scatter(x=btc_usd_price_kraken.index, y=btc_usd_price_kraken['Weighted Price'])
py.iplot([btc_trace])

image

Для визуализации здесь используется Plotly. Это менее традиционный подход по сравнению с более авторитетными python-библиотеками визуализации, такими, как Matplotlib, но на мой взгляд, Plotly представляет собой отличный выбор, поскольку позволяет создавать полностью интерактивные графики за счет применения D3.js. В итоге можно без каких-либо настроек получить на выходе приятные визуальные диаграммы. Кроме того, Plotly прост в изучении и его результаты легко вставляются в веб-страницы.

Разумеется, следует всегда помнить о необходимости сравнивать полученные визуализации с публично-доступными графиками криптовалютных цен (например, на Coinbase) для базовой проверки достоверности скачанных данных.

2.3 Запрашиваем ценовые данные с других BTC-бирж

Вы возможно заметили неувязки в этом наборе: график проседает в нескольких местах до нуля, особенно в конце 2014 года и начале 2016. Эти падения встречаются именно в наборе данных Kraken, и мы очевидно не захотим, чтобы они нашли отражение в нашем итоговым ценовом анализе.

Природа биткоин-бирж такова, что цены определяются спросом и предложением, и потому ни одна из существующих бирж не может претендовать на то, что ее котировки отражают единственно верную, «эталонную» цену Биткоина. Для учета этого недостатка, а также устранения проседания цены на графике, которое скорее всего связано с техническими неполадками или ошибками в наборе данных, мы дополнительно соберем данные еще с трех крупных биткоин-бирж для расчета совокупного индекса цен на биткоин.

Для начала давайте скачаем данные с каждой биржи в словарь фреймов данных.

# Pull pricing data for 3 more BTC exchanges
exchanges = ['COINBASE','BITSTAMP','ITBIT']

exchange_data = {}

exchange_data['KRAKEN'] = btc_usd_price_kraken

for exchange in exchanges:
    exchange_code = 'BCHARTS/{}USD'.format(exchange)
    btc_exchange_df = get_quandl_data(exchange_code)
    exchange_data[exchange] = btc_exchange_df

2.4 Объединяем все ценовые данные в один фрейм данных

Далее, мы определим простую функцию, объединяющую аналогичные столбцы каждого фрейма данных в новый комбинированный фрейм.

def merge_dfs_on_column(dataframes, labels, col):
    '''Merge a single column of each dataframe into a new combined dataframe'''
    series_dict = {}
    for index in range(len(dataframes)):
        series_dict[labels[index]] = dataframes[index][col]
        
    return pd.DataFrame(series_dict)

А теперь объединим все фреймы данных на базе столбца Weighted Price (средневзвешенная цена).

# Merge the BTC price dataseries' into a single dataframe
btc_usd_datasets = merge_dfs_on_column(list(exchange_data.values()), list(exchange_data.keys()), 'Weighted Price')

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

btc_usd_datasets.tail()

image

Цены выглядят как положено: они находятся в схожих пределах, но есть небольшие различия, основанные на соотношении спроса/предложения на каждой отдельной бирже.

2.5 Визуализируем ценовые наборы данных

Следующий логически вытекающий шаг — визуализировать сравнение полученных наборов данных. Для этого определим вспомогательную функцию, предоставляющую возможность сгенерировать график на основе фрейма данных с помощью однострочной команды.

def df_scatter(df, title, seperate_y_axis=False, y_axis_label='', scale='linear', initial_hide=False):
    '''Generate a scatter plot of the entire dataframe'''
    label_arr = list(df)
    series_arr = list(map(lambda col: df[col], label_arr))
    
    layout = go.Layout(
        title=title,
        legend=dict(orientation="h"),
        xaxis=dict(type='date'),
        yaxis=dict(
            title=y_axis_label,
            showticklabels= not seperate_y_axis,
            type=scale
        )
    )
    
    y_axis_config = dict(
        overlaying='y',
        showticklabels=False,
        type=scale )
    
    visibility = 'visible'
    if initial_hide:
        visibility = 'legendonly'
        
    # Form Trace For Each Series
    trace_arr = []
    for index, series in enumerate(series_arr):
        trace = go.Scatter(
            x=series.index, 
            y=series, 
            name=label_arr[index],
            visible=visibility
        )
        
        # Add seperate axis for the series
        if seperate_y_axis:
            trace['yaxis'] = 'y{}'.format(index + 1)
            layout['yaxis{}'.format(index + 1)] = y_axis_config    
        trace_arr.append(trace)

    fig = go.Figure(data=trace_arr, layout=layout)
    py.iplot(fig)

Ради краткости, я не буду подробно вдаваться в работу вспомогательной функции. Если вам интересно узнать про нее больше, обратитесь к документации Pandas и Plotly.

Мы можем с легкостью сгенерировать график для ценовых данных по биткоину.

# Plot all of the BTC exchange prices
df_scatter(btc_usd_datasets, 'Bitcoin Price (USD) By Exchange')

image

2.6 Очищение и объединение ценовых данных

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

Давайте удалим из фрейма все нулевые значения, поскольку мы знаем, что цена биткоина никогда не была равна нулю в рамках рассматриваемого нами временного периода.

# Remove "0" values
btc_usd_datasets.replace(0, np.nan, inplace=True)

Построив график заново, получим более опрятную кривую, без каких-либо резких провалов.

# Plot the revised dataframe
df_scatter(btc_usd_datasets, 'Bitcoin Price (USD) By Exchange')

image

А теперь можем вычислить новый столбец, содержащий среднюю ежедневную цену биткоина на основе данных всех бирж.

# Calculate the average BTC price as a new column
btc_usd_datasets['avg_btc_price_usd'] = btc_usd_datasets.mean(axis=1)

Этот новый столбец и есть наш ценовой индекс биткоина! Давайте построим по нему график, чтобы убедиться, что он выглядит нормально.

# Plot the average BTC price
btc_trace = go.Scatter(x=btc_usd_datasets.index, y=btc_usd_datasets['avg_btc_price_usd'])
py.iplot([btc_trace])

image

Да, выглядит хорошо. Мы воспользуемся объединенной ценовой серией в дальнейшем для конвертации биржевых курсов других криптовалют в доллар США.

Этап 3. Получение ценовых данных альткоинов

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

3.1 Определяем вспомогательные функции для работы с Poloniex API

Для получения данных по альткоинам мы воспользуемся API Poloniex. В этом нам помогут две вспомогательные функции, скачивающие и кэширующие JSON данные, переданные этим API.

Для начала мы определим get_json_data, которая будет скачивать и кэшировать JSON-данные по предоставленному URL.

def get_json_data(json_url, cache_path):
    '''Download and cache JSON data, return as a dataframe.'''
    try:        
        f = open(cache_path, 'rb')
        df = pickle.load(f)   
        print('Loaded {} from cache'.format(json_url))
    except (OSError, IOError) as e:
        print('Downloading {}'.format(json_url))
        df = pd.read_json(json_url)
        df.to_pickle(cache_path)
        print('Cached {} at {}'.format(json_url, cache_path))
    return df

Далее определим функцию, генерирующую HTTP-запросы к API Poloniex, и после вызывающую get_json_data, которая, в свою очередь, сохраняет запрошенные данные.

base_polo_url = 'https://poloniex.com/public?command=returnChartData&currencyPair={}&start={}&end={}&period={}'
start_date = datetime.strptime('2015-01-01', '%Y-%m-%d') # get data from the start of 2015
end_date = datetime.now() # up until today
pediod = 86400 # pull daily data (86,400 seconds per day)

def get_crypto_data(poloniex_pair):
    '''Retrieve cryptocurrency data from poloniex'''
    json_url = base_polo_url.format(poloniex_pair, start_date.timestamp(), end_date.timestamp(), pediod)
    data_df = get_json_data(json_url, poloniex_pair)
    data_df = data_df.set_index('date')
    return data_df

Она берет строку с указанием криптовалютной пары (например BTC_ETH) и возвращает фрейм данных, содержащий исторические данные по ее биржевому курсу.

3.2 Скачивание торговых данных с Poloniex

Большинство альткоинов нельзя приобрести напрямую за доллары США. Для их приобретения люди часто покупают биткоины и меняют их на альткоины на биржах. Поэтому мы скачаем биржевые курсы BTC к каждому коину и воспользуемся данными по цене BTC чтобы вычислить стоимость альткоинов в USD.

Мы скачаем биржевые данные для девяти наиболее популярных криптовалют — Ethereum, Litecoin, Ripple, Ethereum Classic, Stellar, Dashcoin, Siacoin, Monero и NEM.

altcoins = ['ETH','LTC','XRP','ETC','STR','DASH','SC','XMR','XEM']

altcoin_data = {}
for altcoin in altcoins:
    coinpair = 'BTC_{}'.format(altcoin)
    crypto_price_df = get_crypto_data(coinpair)
    altcoin_data[altcoin] = crypto_price_df

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

altcoin_data['ETH'].tail()

image

3.3 конвертация цен в доллары США

Теперь мы можем сопоставить данные по ценовым парам с нашим ценовым индексом биткоина для прямого получения исторических данных по стоимости альткоинов в долларах США.

# Calculate USD Price as a new column in each altcoin dataframe
for altcoin in altcoin_data.keys():
    altcoin_data[altcoin]['price_usd'] =  altcoin_data[altcoin]['weightedAverage'] * btc_usd_datasets['avg_btc_price_usd']

С помощью этого кода мы создали новый столбец во фрейме данных каждого альткоина с долларовыми ценами за коин.

Далее, можем повторно воспользоваться определенной ранее функцией merge_dfs_on_column для создания фрейма данных, содержащего долларовые цены для каждой криптовалюты.

# Merge USD price of each altcoin into single dataframe 
combined_df = merge_dfs_on_column(list(altcoin_data.values()), list(altcoin_data.keys()), 'price_usd')

Вот так вот просто. А теперь давайте также добавим цены на биткоин в последний столбец комбинированного фрейма данных.

# Add BTC price to the dataframe
combined_df['BTC'] = btc_usd_datasets['avg_btc_price_usd']

А теперь у нас появился единый фрейм, содержащий ежедневные долларовые цены для десяти изучаемых нами криптовалют.

Давайте повторно воспользуемся заданной ранее функцией df_scatter, чтобы нарисовать сравнительный график изменения цен на криптовалюты.

# Chart all of the altocoin prices
df_scatter(combined_df, 'Cryptocurrency Prices (USD)', seperate_y_axis=False, y_axis_label='Coin Value (USD)', scale='log')

image

Отлично! График позволяет довольно наглядно оценить динамику биржевых курсов каждой криптовалюты за последние несколько лет.

Обратите внимание, что мы пользуемся логарифмической шкалой ординат, потому что она позволяет нам уместить все валюты на одной диаграмме. Но при желании вы можете попробовать различные значения параметров (такие, как scale='linear') чтобы взглянуть на данные с другой точки зрения.

3.4 Анализ корреляции

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

Но предчувствие, основанное на внешнем сходстве ничем не лучше простой догадки до тех пор, пока мы не можем подкрепить ее статистическими данными.

Мы можем проверить нашу корреляционную гипотезу с помощью метода corr() из набора Pandas, вычислив с его помощью коэффициент корреляции Пирсона всех столбцов фрейма по отношению друг к другу.

Исправление от 8/22/2017 — Эта часть работы была пересмотрена. Теперь для вычисления коэффициентов корреляции вместо абсолютных ценовых значений используются процентные величины их ежедневного изменения.

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

Для начала вычислим корреляцию в 2016 году.

# Calculate the pearson correlation coefficients for cryptocurrencies in 2016
combined_df_2016 = combined_df[combined_df.index.year == 2016]
combined_df_2016.pct_change().corr(method='pearson')

image

Теперь у нас повсюду коэффициенты. Значения, близкие к 1 или -1 говорят, что между временными рядами есть сильная прямая или обратная корреляции соответственно. Близкие к нулю коэффициенты означают, что величины не коррелируют, и меняются независимо друг от друга.

Для визуализации результатов нам понадобится создать еще одну вспомогательную функцию визуализации.

def correlation_heatmap(df, title, absolute_bounds=True):
    '''Plot a correlation heatmap for the entire dataframe'''
    heatmap = go.Heatmap(
        z=df.corr(method='pearson').as_matrix(),
        x=df.columns,
        y=df.columns,
        colorbar=dict(title='Pearson Coefficient'),
    )
    
    layout = go.Layout(title=title)
    
    if absolute_bounds:
        heatmap['zmax'] = 1.0
        heatmap['zmin'] = -1.0
        
    fig = go.Figure(data=[heatmap], layout=layout)
    py.iplot(fig)

correlation_heatmap(combined_df_2016.pct_change(), "Cryptocurrency Correlations in 2016")

image

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

О чем нам говорит этот график? По сути, он показывает, что статистически значимая связь между ценовыми флуктуациями различных криптовалют в 2016 году невелика.

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

combined_df_2017 = combined_df[combined_df.index.year == 2017]
combined_df_2017.pct_change().corr(method='pearson')

image

Полученные коэффициенты говорят о наличии более значимой корреляции. Достаточно ли она сильна чтобы воспользоваться этим фактом для инвестирования? Определенно нет.

Но следует, однако, обратить внимание на то, что почти все криптовалюты в целом стали больше коррелировать друг с другом.

correlation_heatmap(combined_df_2017.pct_change(), "Cryptocurrency Correlations in 2017")

image

И это довольно интересное наблюдение.

Почему это происходит?

Хороший вопрос. Не могу сказать наверняка.

Первая мысль, которая приходит на ум: причина заключается в том, что хедж-фонды недавно начали открыто торговать на криптовалютных рынках. [1][2] Подобные фонды обладают гораздо большими объемами капиталов, нежели трейдеры средней руки, и если они защищаются от рисков, распыляя свои средства по множеству криптовалют и пользуются схожими стратегиями торговли для каждой из них, основываясь на независимых переменных (как это делают, например, на рынке акций), то вполне логичным следствием такого подхода может стать появление тренда увеличения корреляций.

Углубленный анализ: XRP и STR

Например, одна из тенденции косвенно подтверждает приведенные рассуждения. XRP (токен Ripple) коррелирует с другими альткоинами меньше всего. Но есть одно примечательное исключение — STR (токен Stellar, официальное называется «Люмены»), коэффициент корреляции которого с XRP равен 0.62.

Что интересно, как Stellar, так и Ripple представляют собой довольно похожие финтех-платформы, деятельность которых направлена на упрощение процесса международных межбанковских платежей.

Вполне реальной мне видится ситуация, в которой некоторые состоятельные игроки и хедж-фонды используют схожие стратегии торговли вложенными в Stellar и Ripple средствами, поскольку оба сервиса, стоящие за этими токенами очень похожи по своей сути. Это предположение может объяснить, почему XRP значительно сильнее коррелирует с STR, нежели с другими криптовалютами.

Ваша очередь

Однако это объяснение во многом лишь умозрительное заключение. Но может быть у вас получится лучше? Фундамент, который мы заложили в этой работе позволяет продолжить исследование данных в самых разных направлениях.

Вот вам некоторые идеи на проверку:

  • Добавить к анализу данные по большему количеству криптовалют.
  • Скорректировать временные рамки и степень детализации корреляционного анализа, рассмотрев тренды в более детально, или наоборот, в более общих чертах.
  • Поискать тренды в объемах торговли и/или наборах данных по майнингу блокчейнов. Соотношения объемов купли/продажи больше подходят для предсказания ценовых флуктуаций, нежели необработанные ценовые данные.
  • Добавить ценовые данные по акциям, товару и сырью, фиатным валютам, чтобы выяснить, какие из этих активов коррелируют с криптовалютами. (Но всегда помните старую добрую поговорку «Корреляция еще не подразумевает причинную связь»).
  • Выразить количественно величину ажиотажа вокруг отдельных криптовалют с помощью Event Registry, GDELT и Google Trends.
  • Использовав машинное обучение, потренируйте программу для анализа данных с целью предсказания динамики цен. Если позволяют амбиции, вы могли бы даже попытаться сделать это с помощью рекуррентной нейронной сети.
  • Воспользуйтесь своим анализом для создания автоматизированного бота-трейдера, торгующего на таких площадках, как Poloniex и Coinbase с помощью соответствующих API. Но будьте осторожны: плохо оптимизированный торговый бот может быстро лишить вас всех имеющихся средств.
  • Поделитесь своими находками! Лучшая особенность Биткоина и других криптовалют в целом заключается в том, что их децентрализованная природа делает их более свободными и демократичными, по сравнению практически с любыми другими активами. Поделитесь своими наработками со всеми, поучаствуйте в жизни сообщества, и может быть, даже напишите об этом блог-пост.

HTML-версия этого python-журнала доступна здесь.

Надеюсь, теперь вы получили в свое распоряжение навыки, позволяющие вам произвести собственный анализ и критически взглянуть на любую статью-рассуждение о криптовалютах, которые будут попадаться вам в будущем, особенно тех, авторы которых не позаботились представить подкрепляющих свои теории данных.

Спасибо за чтение и, пожалуйста, оставляйте комментарии, если у вас есть какие-либо идеи, предложения или критика. Если у вас возникают какие-либо проблемы с кодом, вы можете сообщить о ней в Github-репозитории.

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

image

Автор: Wirex

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js