🗿

C#でBinanceAPIを利用し、スキャルピングBot実験して失敗

2021/12/14に公開

概要

BinanceAPIを使用して仮想通貨スキャルピング自動Bot実験を行った。毎日少しでも利益が出るようであれば成功とし、資金が減っていくなら失敗、とした。

結論

失敗。動作させればさせるほど原資が減って行くことになった。
スキャルピング発注条件を変化させれば利益がでる組み合わせが存在しうるので、今後の取り組むこととする。

方針

逆張りのスキャルピングとする。
価格が急落した場合、ほとんどの場合、調整のためいったん少しだけ戻るため、そこの逆張りで利益を狙うものとする。

  • -0.5%変化したら買い、買った価格から+0.3%で利確する。
  • 買った価格から-2%変化すると損切する。

直前3日間のデータで事前に実験し、利益が出そうだったので、今回はこの組み合わせにした。機械学習させて常時このパラメータを変更させれば利益がでる組み合わせが存在するかもしれない。また、順張りにしても良いだろうし、仮想通貨は通貨の組み合わせを逆にするだけで売り注文になるので売り注文から入ってもよい。

種類 注文 買い注文を入れる変化率 利確 損切
逆張り -0.5% +0.3% -2% 今回
逆張り -0.5% +0.3% -2%
順張り +0.5% +0.3% -2%
順張り +0.5% +0.3% -2%

使用ライブラリ

BinanceはAPIが提供されているため、それを利用する。またC#用のOSSライブラリがWrapperとしてBinance.Netがあるので、それも利用した。

https://github.com/JKorf/Binance.Net

このBinance.Netライブラリは仕様ドキュメントがほとんどないため、Binanceが提供しているAPIと合わせてコードを読んで理解するのが良い。

https://github.com/binance/binance-spot-api-docs/blob/master/rest-api.md

メインフロー

  • APIKeyを設定
  • 購入通貨のSymbolリストを取得
  • 以下を3分間隔でループさせる
  • 3分間隔で価格データを取得
  • 発注価格条件を満たしていたら発注する
internal void Execute()
{
    try
    {
        #region API Keyの設定

        BinanceClient.SetDefaultOptions(new BinanceClientOptions()
        {
            ApiCredentials = new ApiCredentials(BinanceConfiguration.GetInstance().GetKey(), BinanceConfiguration.GetInstance().GetSecretKey()),
            LogLevel = LogLevel.Error,
            LogWriters = new List<ILogger> { new ConsoleLogger() }
        });

        #endregion API Keyの設定

        File.Delete(LOGPATH);

        using (var client = new BinanceClient())
        {
            //Symbol取得
            var symbols = GetSymbols(client);

            //以下をループさせる
            while (true)
            {
                foreach (var symbol in symbols)
                {
                    //Binance.Net API を使用して3分間隔で価格データを取得
                    var lines = client.Spot.Market.GetKlinesAsync(symbol, KlineInterval.ThreeMinutes, DateTimeOffset.UtcNow.DateTime.AddMinutes(-3), DateTimeOffset.UtcNow.DateTime);
                    lines.Wait();

                    IBinanceKline tickData = null;
                    //注文すべき価格か判定
                    if (isValidRate(symbol, lines, out tickData))
                    {
                        //注文実行
                        ExecuteOrder(client, symbol);
                    }
                }
                File.AppendAllText(LOGPATH, $"{DateTime.Now} Executed,{ Environment.NewLine}");

                //3分間隔で実行する。タイミングが悪いと動作しないことがあったので、ランダム要素を入れる
                Thread.Sleep(new Random().Next(60000, 180000));
            }
        }
    }
    catch (Exception ex)
    {
        File.AppendAllText(LOGPATH, $"{DateTime.Now} {ex.Message}");
    }
}

購入通貨のSymbolリストを取得

Symbolとは取引通貨のキーであり、"ADA/BNB"のように通貨の同士の組み合わせで表現される。

  • BinanceはBNBとの取引は手数料が安くなる。通常は取引量の0.1%だが、0.075%となる。今回はBNBは含まれている通貨を対象とした。
  • また取引量が少ないと売買自体が成立しないため、取引量が1000BNB相当以上の通貨を対象とした。
private IEnumerable<string> GetSymbols(BinanceClient client)
{
    #region case 全コイン対象 ただしBNB取引量1000以上

    var info = client.Spot.System.GetExchangeInfoAsync();
    info.Wait();

    var candidateSymbols = info.Result.Data.Symbols.Select(n => n.Name).Where(n => n.EndsWith("BNB")).ToList();

    var symbols = new List<string>();
    foreach (var symbol in candidateSymbols)
    {
        //通貨量把握のため、Symbol情報を取得する
        var exchangeInfo = client.Spot.Market.GetTickerAsync(symbol);
        exchangeInfo.Wait();

        //通貨量が1000以上だったら対象とする
        if (exchangeInfo.Result.Data.QuoteVolume >= 1000)
        {
            symbols.Add(symbol);
        }
    }
    symbols.Sort();

    #endregion case 全コイン対象 ただしBNB取引量1000以上

    return symbols;
}

発注価格条件を満たしていたら発注する

  • 購入判断の際、変化率を決めておき、設定変化を満たしたら実行可能とする
  • あまりに急変した場合は除外する。反発せずにそのまま急落すると考えられるため。
        private double DOWNRATE = 99.7; //急落時判定割合 99.7 → 0.3%下落に購入する
        private double LIMITRATE = 98; //急落時未判定割合 98 → 2% 下落した場合は購入しない
        private bool isValidRate(string symbol, Task<WebCallResult<IEnumerable<IBinanceKline>>> lines, out IBinanceKline tick)
        {
            tick = null;
            if (lines.Result.Data.Count() == 0) return false;

            //最新価格のOpenとCloseの差(変動率)を計算
            tick = lines.Result.Data.Last();
            var askRate = ((double)tick.Close / (double)tick.Open) * 100;

            File.AppendAllText(LOGPATH, $"{symbol},{askRate},{(double)tick.Close},{(double)tick.Open}{ Environment.NewLine}");

            //変動率が一定範囲だったら発注OKとする
            if (LIMITRATE < askRate && askRate < DOWNRATE)
            {
                File.AppendAllText(LOGPATH, $"{symbol},{askRate},{(double)tick.Close},{(double)tick.Open}{ Environment.NewLine}");
                return true;
            }

            return false;
        }

注文実行

設定したい価格に合わせて購入する量、利確する価格、損切する価格求めて、APIを実行する。

private void ExecuteOrder(BinanceClient client, string symbol)
{
    var allPrices = client.Spot.Market.GetAllBookPricesAsync();
    allPrices.Wait();
    var priceInfo = allPrices.Result.Data.Where(n => n.Symbol == symbol).Single();
    var BASEQUANTITY = (decimal)0.1;//fixed

    var targetQuantity = BASEQUANTITY / ((decimal)priceInfo.BestAskPrice * (decimal)0.99925);//手数料を考慮して購入価格設定
    var targetPrice = (decimal)priceInfo.BestAskPrice * (decimal)1.00375;//0.3%上昇した時の価格 + 0.075%(手数料);
    var stopLossPrice = (decimal)priceInfo.BestAskPrice * (decimal)0.98075;//2%下落した時の価格 + 0.075%(手数料);

    var exchangeInfo = client.Spot.System.GetExchangeInfoAsync();
    exchangeInfo.Wait();
    var symbolExchangeInfo = exchangeInfo.Result.Data.Symbols.Where(n => n.Name == symbol).Single();

    ArrangePrice(symbolExchangeInfo, ref targetQuantity, ref targetPrice, ref stopLossPrice);

    //BNB 0.1 に対する対象通貨のQuantityと対象通貨の値上がり後の価格
    var callResult = client.Spot.Order.PlaceOrderAsync(
        symbol,
        OrderSide.Buy,
        OrderType.Market,
        quantity: (decimal)targetQuantity);

    callResult.Wait();

    if (!callResult.Result.Success)
    {
        // Call failed, check callResult.Error for more info
        File.AppendAllText(LOGPATH, $"{DateTime.Now} Limit order FAIL : {symbol},{OrderSide.Buy},Quantity:{targetQuantity},AskPrice:{priceInfo.BestAskPrice},Target:{targetPrice},StopLossPrice:{stopLossPrice}, {callResult.Result.Error},{ Environment.NewLine}");
        return;
    }
    else
    {
        // Call succeeded, callResult.Data will have the resulting data
        File.AppendAllText(LOGPATH, $"{DateTime.Now} Limit order SUCCESS : {symbol},{OrderSide.Buy},Quantity:{targetQuantity},AskPrice:{priceInfo.BestAskPrice},Target:{targetPrice},StopLossPrice:{stopLossPrice}, { Environment.NewLine}");
    }
    //Limit order
    //var callResultLimit = client.Spot.Order.PlaceTestOrderAsync(symbol, OrderSide.Sell, OrderType.Limit, quantity: (decimal)targetQuantity, price: (decimal)targetPrice, timeInForce: TimeInForce.GoodTillCancel);

    //Stop Loss Limit : StopPriceまで下がったらLimitOrderを発行する
    //var callResultLimit = client.Spot.Order.PlaceTestOrderAsync(
    //    symbol,
    //    OrderSide.Sell,
    //    OrderType.StopLossLimit,
    //    quantity: (decimal)targetQuantity,
    //    price: (decimal)targetPrice,
    //    stopPrice: (decimal)stopLossPrice,
    //    timeInForce: TimeInForce.GoodTillCancel);

    //callResultLimit.Result.Data.OrderId

    var callResultOCO = client.Spot.Order.PlaceOcoOrderAsync(symbol,
        OrderSide.Sell,
        quantity: (decimal)targetQuantity,
        price: (decimal)targetPrice,
        stopPrice: (decimal)stopLossPrice,
        stopLimitPrice: (decimal)stopLossPrice,
        stopLimitTimeInForce: TimeInForce.GoodTillCancel);

    callResultOCO.Wait();

    if (!callResultOCO.Result.Success)
    {
        // Call failed, check callResult.Error for more info
        File.AppendAllText(LOGPATH, $"{DateTime.Now} OCO FAIL : {symbol},{OrderSide.Sell},Quantity:{targetQuantity},AskPrice:{priceInfo.BestAskPrice},Target:{targetPrice},StopLossPrice:{stopLossPrice}, {callResultOCO.Result.Error},{ Environment.NewLine}");
        return;
    }
    else
    {
        // Call succeeded, callResult.Data will have the resulting data
        File.AppendAllText(LOGPATH, $"{DateTime.Now} OCO SUCCESS : {symbol},{OrderSide.Sell},Quantity:{targetQuantity},AskPrice:{priceInfo.BestAskPrice},Target:{targetPrice},StopLossPrice:{stopLossPrice}, { Environment.NewLine}");
    }
}

購入量調整

  • 購入したい通貨の取引量や価格をAPIに合わせてを設定する
  • 通貨ごとに取引量、価格とも桁数が決まっており、桁数が大きいと発注エラーとなるため、桁数を整える
private void ArrangePrice(Binance.Net.Objects.Spot.MarketData.BinanceSymbol symbolExchangeInfo, ref decimal targetQuantity, ref decimal targetPrice, ref decimal stopLossPrice)
{
    var symbolInfo = symbolExchangeInfo;

    try
    {
        ////cut digit
        var minQty = symbolExchangeInfo.LotSizeFilter.MinQuantity;

        if (minQty == (decimal)1.0)
        {
            targetQuantity = (int)targetQuantity;
        }
        else
        {
            //arrange digit
            var digit2 = (decimal)Math.Pow(10, minQty.ToString().IndexOf('1') - 1);
            targetQuantity = Math.Truncate(targetQuantity * digit2) / digit2;
        }
    }
    catch (Exception e)
    {
        throw e;
    }

    try
    {
        ////cut digit
        var minPrice = symbolExchangeInfo.PriceFilter.MinPrice;

        if (minPrice == (decimal)1.0)
        {
            targetPrice = (int)targetPrice;
        }
        else
        {
            //arrange digit
            var digit2 = (decimal)Math.Pow(10, minPrice.ToString().IndexOf('1') - 1);
            targetPrice = Math.Truncate(targetPrice * digit2) / digit2;
        }
    }
    catch (Exception e)
    {
        throw e;
    }

    try
    {
        ////cut digit
        var minPrice = symbolExchangeInfo.PriceFilter.MinPrice;

        if (minPrice == (decimal)1.0)
        {
            stopLossPrice = (int)stopLossPrice;
        }
        else
        {
            //arrange digit
            var digit2 = (decimal)Math.Pow(10, minPrice.ToString().IndexOf('1') - 1);
            stopLossPrice = Math.Truncate(stopLossPrice * digit2) / digit2;
        }
    }
    catch (Exception e)
    {
        throw e;
    }
}

今後

結果は失敗だったが、今後ともいろいろ実験してる予定である。パラメータを少しずつ変える、過去データを学習させる、3通貨間アービトラージ、その他取引所間でのアービトラージなどを気長に試してみる。

Discussion