Как стать автором
Обновить

Доступ к API Binance, KuCoin и Huobi на C#

Время на прочтение8 мин
Количество просмотров15K

Получать данные через RestAPI биржи напрямую из клиентского кода удобно, но по любому существуют ситуации, когда все таки лучше или даже, бывает необходимо предварительно обрабатывать данные на своем сервере, сохранять в своей базе данных и уже после этого предоставлять доступ к этим обработанным данным клиентскому приложению через, опять же, свой 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 пар на каждой, и среди них не мало неликвида, маленькие объёмы или вообще ноль, или цена дергается как-то искусственно, не рыночным образом, зачем нам такие, пусть они даже и будут в списке, но где то внизу, там куда долго мотать, а сверху красавицы, так как в примере ниже.

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

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

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

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

Для работы с Базой Данных будем использовать 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, в котором застолбим необходимость реализации в наследниках (классах для конкретных бирж) публичных свойств ID и Name (номер и название), а так же трех защищенных методов ToProductProcessProducts и GetKlines, в них будем обращаться к API биржи за списком продуктов, и за свечами для каждой пары по которым вычислим volatility и liquidity этой пары, все это конечно сохраним уже потом в базе. Кроме этого в нашем абстрактном классе Exchange реализуем публичный метод, который будет запускать длинный процесс получения данных и вычисления статистики в отдельном потоке, что бы не завешивать главное окно программы. Наследниками класса Exchange у нас по плану будут 3-и класса BinanceKucoin и Huobi

using CryptoExchange.Net.CommonObjects;

namespace caSever01
{
    public abstract class Exchange
    {
        public abstract int ID { get; }
        public abstract string Name { get; }
        public void UpdateProductsStat()
        {
            Task.Run(() => {
                ProcessProducts();
            });
        }
        protected abstract Product ToProduct(Object p);
        protected abstract void ProcessProducts();
        protected abstract List<Kline> GetKlines(string symbol,
                                int IntervarInMinutes, int PeriodInDays);
    }
}

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

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

using Binance.Net.Clients;
using Binance.Net.Objects.Models.Spot;
using CryptoExchange.Net.CommonObjects;

namespace caSever01
{
    public class Binance : Exchange
    {
        public override int ID => 1;
        public override string Name => "Binance";

        BinanceClient client = new();

        protected override Product ToProduct(object p)
        {
            BinanceProduct binaProd = (BinanceProduct)p;
            Product product = new();
            product.symbol = binaProd.Symbol;
            product.exchange = ID;
            product.baseasset = binaProd.BaseAsset;
            product.quoteasset = binaProd.QuoteAsset;

            return product;
        }
... ... ...

Указываем номер и имя биржи, далее создаем объект client типа BinanceClient, тут даже без авторизации, для получения биржевых маркет данных авторизации не требуется.

Метод ToProduct преобразует специфический для Бинанс объект типа BinanceProduct в наш модельный Product и все, теперь переходим к главному:

protected override void ProcessProducts()
{
    var r = client.SpotApi.ExchangeData.GetProductsAsync().Result;
    if (r.Success)
    {
        foreach (var p in r.Data)
        {
            if (p.Status == "TRADING")
            {
                Product product = ToProduct(p);
                List<Kline> klines = GetKlines(p.Symbol, 1, 5);
                if (klines.Count > 0)
                {
                    product.CalcStat(klines);
                    product.SaveStat();
                }
                Thread.Sleep(1000);
            }
        }
    }
}

В методе ProcessProducts первым делом получаем список торговых пар вот таким вызовом:
var r = client.SpotApi.ExchangeData.GetProductsAsync().Result;
и далее уже для каждой пары получаем свечи в методе GetKlines, вычисляем статистику продукта, передавая эти свечи ему в метод CalcStat и сохраняем в базе SaveStat.

На каждой итерации цикла по продуктам делаем в конце
Tread.Sleep(1000); 
это попытка избежать ошибки: Too Many Requests, но на KuCoin она почему то все  равно периодически возникает, хотя по их описанию API превышения количества обращений, вроде как не должно быть.

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

protected override List<Kline> GetKlines(string symbol, int IntervarInMinutes, int PeriodInDays)
{
    List<Kline> klines = new List<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;
}

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

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

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

Классы Kucoin и Huobi абсолютно такие же, мы тут построили наш код таким образом, что для добавления новой биржи надо добавить только один класс (один файлик), все остальное никак не меняется. Другие классы, как говорят, слабо связаны с наследниками Exchange и чтобы реализовать Kucoin можно просто скопировать Binance, поменять там конечно ID, Name ну что то еще по мелочи, вот так:

using Kucoin.Net.Clients;
using Kucoin.Net.Objects.Models.Spot;
using CryptoExchange.Net.CommonObjects;

namespace caSever01
{
    public class Kucoin : Exchange
    {
        public override int ID => 2;
        public override string Name => "Kucoin";

        KucoinClient client = new();

        protected override Product ToProduct(object p)
        {
            KucoinSymbol binaProd = (KucoinSymbol)p;
            Product product = new();
            product.symbol = binaProd.Symbol;
            product.exchange = ID;
            product.baseasset = binaProd.BaseAsset;
            product.quoteasset = binaProd.QuoteAsset;

            return product;
        }
        protected override void ProcessProducts()
        {
            var r = client.SpotApi.ExchangeData.GetSymbolsAsync().Result;
            if (r.Success)
            {
                foreach (var p in r.Data)
                {
                    if (p.EnableTrading)
                    {
                        Product product = ToProduct(p);
                        List<Kline> klines = GetKlines(p.Symbol, 1, 5);
                        if (klines.Count > 0)
                        {
                            product.CalcStat(klines);
                            product.SaveStat();
                        }
                        Thread.Sleep(1000);
                    }
                }
            }
        }
        protected override List<Kline> GetKlines(string symbol,
                                int IntervarInMinutes, int PeriodInDays)
        {
            List<Kline> klines = new List<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;
        }
    }
}

Основное отличие в каждом из этих классов поле client будет своего типа BinancrClientKucoinClient и HuobiClient соответственно.

То-есть разработчики оберток Binance.NETKucoin.NET и Huobi.NET пытаются унифицировать объектные модели этих библиотек так, что бы реализации обращений к методам API всех этих бирж не сильно отличались, но пока это не везде у них получилось.

Например я заметил что в 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() };

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

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

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

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

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

Код проекта найдешь тут

Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
Всего голосов 15: ↑9 и ↓6+3
Комментарии6

Публикации

Истории

Работа

Ближайшие события