this thing made me think today ...

Binance API на C#

В предыдущем видео на канале Кодим Просто мы рассматривали получение данных о курсах крипто валют через Rest full API с бирж Binance, KuCoun и Huobi на JavaScript.

Исходный код тут https://github.com/amizerov/Vidos.BinanceAPI/blob/master/l1/93.htm

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

Ну давайте рассмотрим одну из таких ситуаций, когда получать данные на клиента напрямую с биржи не удобно, в нашем случае это получение списка торговых пар биржи отсортированных по таким показателям как например, ликвидность и волатильность. Вот в примере ниже, мы из клиентского кода на JavaScript обращаемся за списком продуктов биржи к своему Rest full API Web Service по ссылке https://cryptoalert.mizerov.com/api/Products/” + ex

где ex – код биржи.

				
					<!-- Получение отсортированного по ликвидности списка пар
     с любой из 3-х бирж по параметру из своей БД через свой Web API -->
<script>
    function getSymbols(ex) {
        var url = "https://cryptoalert.mizerov.com/api/Products/" + ex;

        //Обращение за данными на свой сервер
        $.getJSON(url, function (data) {

            //Парсинг - преобразование полученных данных в объект
            var paras = JSON.parse(JSON.stringify(data));

            //Очистить список
            $(".symbols").html("");

            //Заполнение списка символов в листбокс
            $.each(paras, function (i, symInfo) {
                var sym = symInfo.symbol.replace("-", "");
                var sli = "<li id=id" + i + " onclick=\"sel(" + i + ", '" + sym + "')\">" + sym + "</li>";
                $(".symbols").append(sli);
            });
        });
    }
</script>
				
			

То есть, еще раз, давайте поставим задачу так, нам хотелось бы видеть список торговых пар, в котором в верхних строках будут, так скажем, наиболее интересные инструменты для трейдера. Ну вы знаете, конечно, что на крипто биржах Binance, KuCoun и Huobi торгуются до 1500 пар на каждой, и среди них не мало неликвида, маленькие объёмы или вообще ноль, или цена дергается как-то искусственно, не рыночным образом, зачем нам такие, пусть они даже и будут в списке, но где то внизу, там куда долго мотать, а сверху красавицы, так вот как посмотрите в примере ниже.

Open / Close High / Low Volume / Close time

    1m
    3m
    5m
    15m
    30m
    1h
    +
    -

    Исходный код этого примера можно посмотреть тут https://github.com/amizerov/Vidos.BinanceAPI/blob/master/l1/93.htm.

    Сейчас я вообще не буду рассматривать реализацию моего Rest full API Web сервиса  cryptoalert.mizerov.com, сразу перейдем к коду на C#, который получит список пар, проанализирует и сохранит в Базе Данных.

    Для начала выберем тип проекта Приложение Windows Forms (Майкрософт), именно этот тип проекта позволит нам использовать фишки последней версии языка C#

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

    • Microsoft.EntityFrameworkCore
    • Microsoft.EntityFrameworkCore.SqlServe

    и сделаем класс для подключения к бд

    				
    					public class CaDb: DbContext
    {
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer("Server=***;Database=CryptoAlert;UID=ca;PWD=***");
        }
        public DbSet< Product >? Products { get; set; }
    }
    				
    			

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

    				
    					    public class Product
        {
            public int Id { get; set; }
            public string symbol { get; set; }
            public int exchange { get; set; }
            public string baseasset { get; set; }
            public string quoteasset { get; set; }
            public double volatility { get; set; }
            public double liquidity { get; set; }
        }
    				
    			

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

    Далее создадим абстрактный класс биржи Exchange, в котором застолбим необходимость реализации в наследниках (классах для конкретных бирж) двух защищенных методов GetProducts и GetKlines, именно в них будем обращаться к API биржи за списком продуктов, и за свечами для каждой пары по которым вычислим volatility и liquidity этой пары, все это конечно сохраним уже потом в базе. Кроме этого в нашем абстрактном классе Exchange реализуем публичный метод, который будет запускать длинный процесс получения данных и вычисления статистики в отдельном потоке, что бы не завешивать главное окно программы. Наследниками класса Exchange у нас по плану будут 3-и класса Binance, Kucoin и Huobi

    				
    					public abstract class Exchange
    {
        public void UpdateProductsStat()
        {
            Task.Run(() => {
                GetProducts();
            });
        }
        protected abstract void GetProducts();
        protected abstract List<kline> GetKlines(string symbol, int IntervarInMinutes, int PeriodInDays);
    }</kline>
    				
    			

    Ну и вот тут подошел момент, когда нам наконец то уже выпало счастье поработать с конкретной биржей из кода на C#, начнем с биржи Binance. Поизучав вопрос, я пришел к выводу, что для работы с API Binance лучше использовать так называемый врапер, обертку в виде набора классов на C# для вызова методов API, нежели напрямую дергать их по http. Как оказалось наибольшей популярностью пользуется библиотека от JKorf, Binance.Net, это бесплатный, как говорится опенсорсик, наш любимый, и вот ссылка на гитхабе https://github.com/JKorf/Binance.Net всем предлагаю пользоваться и поддерживать замечательный проект.

    Для нашей задачи нам надо просто подключить эту библиотеку к проекту через NuGet менеджер пакетов, и можно начать писать код метода GetProducts в классе Binance реализующем абстрактный класс Exchange.

    				
    					using Binance.Net.Clients;
    using CryptoExchange.Net.CommonObjects;
    
    public class Binance : Exchange
    {
        BinanceClient client = new();
    
        protected override void GetProducts()
        {
            var r = client.SpotApi.ExchangeData.GetProductsAsync().Result;
            if (r.Success)
            {
                foreach (var p in r.Data)
                {
                    if (p.Status == "TRADING")
                    {
                        Product product = new Product(p);
                        List<kline> klines = GetKlines(p.Symbol, 1, 5);
                        product.CalcStat(klines);
                        product.SaveStat();
                        Thread.Sleep(1000);
                    }
                }
            }
        }
    ... ... ...</kline>
    				
    			

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

    В 10-ой строке

    var r = client.SpotApi.ExchangeData.GetProductsAsync().Result;

    получаем список продуктов, и далее для тех которые в статусе TRADING, то-есть которые торгуются получаем свечи, вычисляем статистику и сохраняем в базе. Отмечу, что в этом куске кода для создания объекта Product используем конструктор с параметром типа BinanceProduct.

    				
    					public Product(BinanceProduct p)
    {
        symbol = p.Symbol;
        exchange = 1;
        baseasset = p.BaseAsset;
        quoteasset = p.QuoteAsset;
    }
    				
    			

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

    Последний метод, который осталось реализовать для Binance это GetKlines.

    				
    					protected override List<kline> GetKlines(string symbol, int IntervarInMinutes, int PeriodInDays)
    {
        List</kline><kline> klines = new List</kline><kline>();
    
        int m = IntervarInMinutes % 60;
        int h = (IntervarInMinutes - m) / 60;
        TimeSpan klInterval = new TimeSpan(h, m, 0);
                
        var r = client.SpotApi.CommonSpotClient
            .GetKlinesAsync(symbol, klInterval, 
                DateTime.Now.AddDays(-1 * PeriodInDays), DateTime.Now).Result;
        if (r.Success)
        {
            klines = r.Data.ToList();
    
            if (klines.Count == 0)
            {
                Msg.Send(1, $"GetProductStat({symbol})", "i==0");
            }
        }
        else
        {
            Msg.Send(1, $"GetProductStat({symbol})", r.Error!.Message);
        }
        return klines;
    }</kline>
    				
    			

    Для получения свечей пары на Бинансе используем вызов

    var r = client.SpotApi.CommonSpotClient
            .GetKlinesAsync(symbol, klInterval,
                    DateTime.Now.AddDays(-1 * PeriodInDays), DateTime.Now).Result;

    Это спотовые свечи по синтаксису все абсолютно прозрачно. В этом, конечно основное преимущество обертки, в сравнении с прямыми вызовами методов API биржи по http.

    Классы Kucoin и Huobi абсолютно такие же, можно даже просто скопировать, вот так:

    				
    					public class Kucoin : Exchange
    {
        KucoinClient client = new();
    
        protected override void GetProducts()
        {
            var r = client.SpotApi.ExchangeData.GetSymbolsAsync().Result;
            if (r.Success)
            {
                foreach (var p in r.Data)
                {
                    if (p.EnableTrading)
                    {
                        Product product = new Product(p);
                        List<kline> klines = GetKlines(p.Symbol, 1, 5);
                        product.CalcStat(klines);
                        product.SaveStat();
                        Thread.Sleep(1000);
                    }
                }
            }
        }
        protected override List</kline><kline> GetKlines(string symbol, int IntervarInMinutes, int PeriodInDays)
        {
            List</kline><kline> klines = new List</kline><kline>();
    
            int m = IntervarInMinutes % 60;
            int h = (IntervarInMinutes - m) / 60;
            TimeSpan klInterval = new TimeSpan(h, m, 0);
    
            var r = client.SpotApi.CommonSpotClient
                    .GetKlinesAsync(symbol, klInterval, DateTime.Now.AddDays(-1 * PeriodInDays), DateTime.Now).Result;
    
            if (r.Success)
            {
                klines = r.Data.ToList();
    
                if (klines.Count == 0)
                {
                    Msg.Send(2, $"GetProductStat({symbol})", "i==0");
                }
            }
            else
            {
                Msg.Send(2, $"GetProductStat({symbol})", r.Error!.Message);
            }
            return klines;
        }
    }
    </kline>
    				
    			

    То-есть разработчики оберток Binance.NET, Kucoin.NET и Huobi.NET унифицировали объектные модели этих библиотек так, что реализации обращений к методам API всех этих бирж почти не отличаются.
    Единственное отличие в каждом из этих классов поле client будет своего типа BinancrClient, KucoinClient и HuobiClient соответственно. Ну еще одно отличие я заметил в Huobi вызов получения свечей не хочет принимать параметры DateTimeFrom, DateTimeTo. 

    				
    					var r = client.SpotApi.CommonSpotClient
        .GetKlinesAsync(symbol, klInterval).Result; 
    // !!! не хочет принимать параметры фром ту как в других биржах
    //, DateTime.Now.AddDays(-1 * PeriodInDays), DateTime.Now).Result;
    
    				
    			

    Используя модную штуку под названием Полиморфизм, на главной форме мы можем объединить все 3-и класса в один массив элементов типа Exchange:

    				
    					public partial class FrmMain : Form
    {
        List<exchange> _exchangeList = new() { new Binance(), new Kucoin(), new Huobi() };
    </exchange>
    				
    			

    И по нажатию кнопки типа btnStart, вызовем обновление статистики торговых пар в цикле:

    				
    					private void btnStart_Click(object sender, EventArgs e)
    {
        foreach(Exchange exchange in _exchangeList)
        {
            exchange.UpdateProductsStat();
        }
    }
    				
    			

    При этом процессы для каждой биржи будут запущены в отдельном своем потоке.

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

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