JKorf / Binance.Net

A C# .netstandard client library for the Binance REST and Websocket Spot and Futures API focusing on clear usage and models
https://jkorf.github.io/Binance.Net/
MIT License
1.05k stars 429 forks source link

Trailing stop #506

Closed Hulkstance closed 3 years ago

Hulkstance commented 4 years ago

I was going to implement a trailing stop-loss orders but there is something that worries me. Right now, the code watches for placed StopLossLimit orders, cancels them and places new ones with the updated prices. The whole procedure is performed on if (!data.Final), which means on each price change. What if the price changes so fast? It probably won't catch up. What could you recommend me? Most of you have probably implemented it, since it's useful.

using Binance.Net.Enums;
using Binance.Net.Interfaces;
using Binance.Net.Objects.Spot.SpotData;
using System;
using System.Linq;

namespace TrailingStopLoss
{
    public class TrailingStopStrategy : StrategyBase
    {
        public TrailingStopStrategy(string apiKey, string secretKey) : base(apiKey, secretKey)
        {
        }

        private const decimal MoveUpPrecentageMarginUnderMarket = 10;
        private const decimal MoveUpStaticThreshold = 100;
        private const decimal MoveUpPercentageThreshold = 10.5m;

        private bool ShouldReorder(string symbol, BinanceOrder order)
        {
            var orderStopPrice = order.StopPrice;
            var currentPrice = Tickers[symbol].LastPrice;

            if (currentPrice <= orderStopPrice)
            {
                // stop-loss is already over last trade price -> it should execute
                //CheckOrder();
                return false;
            }

            // check if last trade price is over thresholds
            if (!OverThresholds(symbol, order)) return false;

            return true;
        }

        private bool OverThresholds(string symbol, BinanceOrder order)
        {
            var orderStopPrice = order.StopPrice;
            var currentPrice = Tickers[symbol].LastPrice;

            if (MoveUpStaticThreshold > 0 &&
                currentPrice > orderStopPrice + MoveUpStaticThreshold)
                return true;

            if (MoveUpPercentageThreshold > 0 &&
                currentPrice > orderStopPrice * (100 + MoveUpPercentageThreshold) / 100)
                return true;

            // TODO: implement hysteresis

            return false;
        }

        private void MoveUp(string symbol, BinanceOrder order)
        {
            var orderStopPrice = order.StopPrice;
            var currentPrice = Tickers[symbol].LastPrice;
            var newStopPrice = currentPrice * (100 - MoveUpPrecentageMarginUnderMarket) / 100;

            // Info
            Logger.WriteLine($"Order stop price: {orderStopPrice} | Current price: {currentPrice} | New stop price: {newStopPrice}");

            // Cancel order
            Client.Spot.Order.CancelOrder(symbol, order.OrderId);
            Logger.WriteLine($"Order {order.OrderId} canceled.");

            // Place order
            var exchangeInfo = Client.Spot.System.GetExchangeInfo().Data;
            if (exchangeInfo != null)
            {
                var binanceSymbol = exchangeInfo.Symbols.First(e => e.Name == symbol);
                var availableBase = Client.General.GetAccountInfo().Data.Balances.First(e => e.Asset == binanceSymbol.BaseAsset).Free; // AccountInfo.Balances.First(e => e.Asset == binanceSymbol.BaseAsset).Free;
                var price = RoundingHelper.ClampPrice(binanceSymbol, newStopPrice, Tickers[symbol].WeightedAveragePrice);
                var quantity = RoundingHelper.ClampQuantity(binanceSymbol, availableBase);

                var quoteQuantity = price * quantity;

                if (quoteQuantity >= binanceSymbol.MinNotionalFilter.MinNotional && availableBase >= quantity)
                {
                    var sellOrderResult = Client.Spot.Order.PlaceOrder(
                        symbol,
                        OrderSide.Sell,
                        OrderType.StopLossLimit,
                        quantity: quantity,
                        price: price,
                        stopPrice: price,
                        timeInForce: TimeInForce.GoodTillCancel);

                    if (sellOrderResult.Success)
                    {
                        Logger.WriteLine($"Order {sellOrderResult.Data.OrderId} placed.");
                    }
                }
            }
        }

        private void Monitor(string symbol, BinanceOrder order)
        {
            if (!ShouldReorder(symbol, order))
                return;

            MoveUp(symbol, order);
        }

        public override void Start(string symbol, KlineInterval interval)
        {
            try
            {
                base.Start(symbol, interval);

                Logger.WriteLine($"Trailing stop-loss strategy | {symbol} | {interval} | Start time: {DateTime.UtcNow.ToLocalTime():yyyy/MM/dd HH:mm}");

                IBinanceKline lastKnownCandle = null;
                bool staticAnalysisLocked = false;
                bool runtimeAnalysisLocked = false;

                var subResult = SocketClient.Spot.SubscribeToKlineUpdates(symbol, interval, data =>
                {
                    if (data.Data.Final)
                    {
                        staticAnalysisLocked = true;

                        lastKnownCandle = data.Data;

                        Logger.WriteLine($"Open time: {lastKnownCandle.OpenTime.ToLocalTime():yyyy/MM/dd HH:mm} | Price: {lastKnownCandle.Close}{Environment.NewLine}");

                        staticAnalysisLocked = false;
                        runtimeAnalysisLocked = false;
                    }
                    else
                    {
                        if (lastKnownCandle != null && lastKnownCandle.Close != data.Data.Close &&
                            !staticAnalysisLocked && !runtimeAnalysisLocked)
                        {
                            runtimeAnalysisLocked = true;

                            lastKnownCandle = data.Data;

                            Logger.WriteLine($"Open time: {lastKnownCandle.OpenTime.ToLocalTime():yyyy/MM/dd HH:mm} | Price: {lastKnownCandle.Close}{Environment.NewLine}", ConsoleColor.Green);

                            // Trailing stop-loss checks
                            var openOrders = Client.Spot.Order.GetOpenOrders(symbol).Data;
                            if (openOrders != null)
                            {
                                var stopLossOrders = openOrders.Where(e => e.Type == OrderType.StopLoss || e.Type == OrderType.StopLossLimit);

                                foreach (var order in stopLossOrders)
                                {
                                    if (!(order.Type == OrderType.StopLoss || order.Type == OrderType.StopLossLimit))
                                        return;

                                    Monitor(symbol, order);
                                }
                            }

                            runtimeAnalysisLocked = false;
                        }
                    }
                });

                subResult.Data.ConnectionLost += () =>
                {
                    runtimeAnalysisLocked = true;

                    Logger.WriteLine($"Network connection was lost | Time: {DateTime.UtcNow.ToLocalTime():yyyy/MM/dd HH:mm}{Environment.NewLine}", ConsoleColor.Red);
                };

                subResult.Data.ConnectionRestored += (e) =>
                {
                    Logger.WriteLine($"Network connection was restored | Time: {DateTime.UtcNow.ToLocalTime():yyyy/MM/dd HH:mm}{Environment.NewLine}", ConsoleColor.Green);
                };
            }
            catch (Exception ex)
            {
                Logger.WriteLine($"Unknown error: {ex.Message} | Stack trace: {ex.StackTrace}{Environment.NewLine}", ConsoleColor.Red);
            }
        }
    }
}
JKorf commented 3 years ago

You'll need to keep the business logic and the data handler separate. You're currently doing all sort of checks and calls within the event handler of the websocket. Doing this will lead to data being received out of date, because new data can only be processed once the event handler for the previous has been completed.

I'd recommend adding the received data to some sort of queue, then have a separate thread/task process the data from that queue.

Amannda123 commented 3 years ago

Instead a queue with a loop around i would highly recommend using Tasks and switch to Event-Driven actions .. just my 2 cents :)

savasadar commented 3 years ago

You'll need to keep the business logic and the data handler separate. You're currently doing all sort of checks and calls within the event handler of the websocket. Doing this will lead to data being received out of date, because new data can only be processed once the event handler for the previous has been completed.

I'd recommend adding the received data to some sort of queue, then have a separate thread/task process the data from that queue.

Hi @JKorf I am implement a algotrading bot too and I am using event handler like this. How to check current price for every tick without using event handler of the websocket? If I use a queue and prices pile up in the queue, Won't this cause delay?

Hulkstance commented 3 years ago

@Amannda123, do you mean the Event-Driven actions used in the microservices architecture? For example: RabbitMQ. https://docs.microsoft.com/en-us/dotnet/architecture/microservices/multi-container-microservice-net-applications/rabbitmq-event-bus-development-test-environment. How do I actually benefit from it?