Хабрахабр

[Из песочницы] Пишем учебное приложение на Go и Javascript для оценки реальной доходности акций. Часть 1 — backend

Давайте попробуем написать небольшую тренировочную, но вполне себе законченную информационную систему, состоящую из серверной части на Go и клиентского веб-приложения на Javascript + Vue JS.

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

  • Деньги съедает инфляция (инфляционный риск)
  • Рубль может обесцениться (курсовой риск)

Было принято решение изучить вопрос и выбрать подходящий инструмент для инвестирования. Основными критериями были надёжность и защита сбережений от указанных выше рисков.
Вопрос я изучил и в результате пришёл к выводу, что единственным адекватным инвестиционным инструментом для жителя России являются акции биржевых фондов (ETF), причём именно те, что торгуются на Московской Бирже.

Согласен, поэтому попытаемся отобразить некоторую условную реальную доходность акций. Таким образом, предлагаю написать учебное приложение, которое бы показывало доходность всех ETF, которые представлены на Московской Бирже.
Вы можете сказать, что эту доходность можно посмотреть на самом сайте биржи, а приложение, хотя бы и учебное, должно быть сколько-нибудь полезным. Наш бэкенд написан на Go и в ходе разработки мы попытаемся применить такие возможности языка, как параллельное исполнение кода, интерфейсы, тестирование и прочее. Под этой условной реальной доходностью, я буду понимать доходность, скорректированную на инфляцию в России.
В первой части статьи мы разберём серверную часть приложения.

Требования ТЗ:

  1. Серверная часть приложения должна предоставлять по запросу данные о котировках всех ETF Московской Биржи и данные об инфляции за все месяцы торгов по каждой бумаге
  2. Серверная часть приложения должна поддерживать несколько поставщиков хранилища данных, переключение между поставщиками не должно требовать изменения кода
  3. Серверная часть приложения должна предоставлять API по протоколу http для получения данных из хранилища

Итак, давайте спроектируем программную архитектуру серверной части нашей системы.

Согласно ТЗ, приложение будет состоять из веб-сервера, который будет предоставлять REST API и отдавать фалы нашего веб-приложения (впоследствии мы напишем SPA на Vue). Во-первых, придумаем структуру пакетов приложения. Кроме того, по ТЗ мы должны сделать несколько пакетов для поставщиков хранилища данных.

Каким образом можно предоставить возможность переключения между поставщиками некоторой функциональности в Go? На этом моменте следует остановиться поподробнее. Таким образом мы должны будем разработать интерфейс (контракт) для пакетов, каждый из которых будет выполнять контракт для своего типа хранилища. Ответ: с помощью интерфейсов. Итоговая структура пакетов будет такая: В статье рассмотрим хранение данных в оперативной памяти, но по аналогии можно легко добавить любую СУБД.

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

Котировки и инфляцию мы будем брать по месяцам, этот масштаб вполне подходит для такого неспекуляционного инструмента, как ETF. Нам потребуются типы данных для котировок акций и инфляции.

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

В итоге в модуль storage мы помещаем типы для хранения котировок и интерфейс:

// Package storage описывает общие требования к поставщику хранилища и используемые типы данных
package storage // Security - ценная бумага
type Security struct { ID string // ticker Name string // полное имя бумаги IssueDate int64 // дата выпуска в обращение Quotes []Quote // котировки
} // Quote - котировка ценной бумаги (цена 'close')
type Quote struct { SecurityID string // ticker Num int // номер измерения (номер месяца) TimeStamp int64 // отметка времени в формате Unix Time Price float64 // цена закрытия
} // Interface - контракт для драйвера хранилища котировок
type Interface interface { InitData() error // инициализирует хранилище данными с сервера Мосбиржи Securities() ([]Security, error) // получить список бумаг с котировками
}

Данные по инфляции для простоты закодируем в модуле сервера:

var inflation = []struct { Year int Values [12]float64
}, }, { Year: 2014, Values: [12]float64{0.59, 0.70, 1.02, 0.90, 0.90, 0.62, 0.49, 0.24, 0.65, 0.82, 1.28, 2.62}, }, { Year: 2015, Values: [12]float64{3.85, 2.22, 1.21, 0.46, 0.35, 0.19, 0.80, 0.35, 0.57, 0.74, 0.75, 0.77}, }, { Year: 2016, Values: [12]float64{0.96, 0.63, 0.46, 0.44, 0.41, 0.36, 0.54, 0.01, 0.17, 0.43, 0.44, 0.40}, }, { Year: 2017, Values: [12]float64{0.62, 0.22, 0.13, 0.33, 0.37, 0.61, 0.07, -0.54, -0.15, 0.20, 0.22, 0.42}, }, { Year: 2018, Values: [12]float64{0.31, 0.21, 0.29, 0.38, 0.38, 0.49, 0.27, 0.01, 0.16, 0.35, 0.50, 0.84}, },
}

В-третьих, давайте опишем конечные точки нашего API. Их будет всего две: для котировок и инфляции. Только метод HTTP GET.

// API нашего сервера http.HandleFunc("/api/v1/securities", securitiesHandler) // список бумаг с котировками http.HandleFunc("/api/v1/inflation", inflationHandler) // инфляция по месяцам

Собственно получение и обработка данных с сайта Мосбиржи осуществляется в методе инициализации. Данные берём согласно справке по API биржи.
На что стоит обратить внимание: мы вынуждены использовать отдельный запрос по каждой ценной бумаге (а их уже пара десятков). Исполнение инициализации данных последовательно, в один поток, заняло бы много времени. Поэтому мы будем использовать гордость Go — горутины. Обратите внимание на следующий кусок кода:

// InitData инициализирует хранилище данными с сервера Мосбиржи
func (s *Storage) InitData() (err error) { securities, err := getSecurities() if err != nil { return err } // объект синхронизации горутин var wg sync.WaitGroup // увеличиваем счётчик горутин по количеству ценных бумаг wg.Add(len(securities)) for _, security := range securities { go func(item storage.Security) { // уменьшаем счётчик перед завершением функции defer wg.Done() var quotes []storage.Quote quotes, err = getSecurityQuotes(item) if err != nil { fmt.Println(item, err) return } item.Quotes = quotes err = s.Add(item) if err != nil { return } }(security) } // ожидаем выполнения всех горутин wg.Wait() return err }

В функции инициализации данных мы распараллелили запросы к серверу. На практике такой парсинг сайтов имеет ряд проблем:

  • Может привести к автоматической блокировке запросов из-за подозрения на DoS
  • Нужно использовать модуль context или управляющий канал для принудительного завершения горутин
  • Нужно использовать канал для возврата ошибки из горутины

Для простоты все эти моменты опускаются.

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

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

Итак давайте напишем наш сервер:

// Package main реализует веб-сервер проетка moex-etf
package main import ( "encoding/json" "fmt" "log" "moex_etf/server/storage" "moex_etf/server/storage/inmemory" "net/http"
) var db storage.Interface func main() { // здесь мы можем, например, добавить проверку флагов запуска или переменной окружения // для выбора поставщика хранилища. выбрали память db = inmemory.New() fmt.Println("Inititalizing data") // инициализация данных хранилища err := db.InitData() if err != nil { log.Fatal(err) } // API нашего сервера http.HandleFunc("/api/v1/securities", securitiesHandler) // список бумаг с котировками http.HandleFunc("/api/v1/inflation", inflationHandler) // инфляция по месяцам // запускаем веб сервер на порту 8080 const addr = ":8080" fmt.Println("Starting web server at", addr) log.Fatal(http.ListenAndServe(addr, nil)) } // обработчик запроса котировок
func securitiesHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET") w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization") if r.Method != http.MethodGet { return } securities, err := db.Securities() if err != nil { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(err.Error())) } err = json.NewEncoder(w).Encode(securities) if err != nil { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(err.Error())) } } // обработчик запроса инфляции
func inflationHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET") w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization") if r.Method != http.MethodGet { return } err := json.NewEncoder(w).Encode(inflation) if err != nil { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(err.Error())) } } // инфляция в России по месяцам
var inflation = []struct { Year int Values [12]float64
}{ { Year: 2013, Values: [12]float64{0.97, 0.56, 0.34, 0.51, 0.66, 0.42, 0.82, 0.14, 0.21, 0.57, 0.56, 0.51}, }, { Year: 2014, Values: [12]float64{0.59, 0.70, 1.02, 0.90, 0.90, 0.62, 0.49, 0.24, 0.65, 0.82, 1.28, 2.62}, }, { Year: 2015, Values: [12]float64{3.85, 2.22, 1.21, 0.46, 0.35, 0.19, 0.80, 0.35, 0.57, 0.74, 0.75, 0.77}, }, { Year: 2016, Values: [12]float64{0.96, 0.63, 0.46, 0.44, 0.41, 0.36, 0.54, 0.01, 0.17, 0.43, 0.44, 0.40}, }, { Year: 2017, Values: [12]float64{0.62, 0.22, 0.13, 0.33, 0.37, 0.61, 0.07, -0.54, -0.15, 0.20, 0.22, 0.42}, }, { Year: 2018, Values: [12]float64{0.31, 0.21, 0.29, 0.38, 0.38, 0.49, 0.27, 0.01, 0.16, 0.35, 0.50, 0.84}, },
}

Приводить здесь код реализации хранилища в памяти не буду, всё доступно на Гитхабе.

Для проверки наш API:

инфляция
котировки

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть