vendredi 19 février 2021

Handling multiple instances/services and their disposal. Messed up design

What is the correct way to manage those instances/services? In my case, IExchangeClient should be disposed at some point and more accurately what's inside it: BinanceClient, BinanceSocketClient and Subject<IObservable<Unit>> (these 3 are third-party library instances). There are many ways where it can be disposed. I tried make it more like ASP.NET Core's way and dispose it in Main(). Maybe there is a cool NuGet package which handles that better, just like ASP.NET Core? I feel like subproject 2's design is completely messed up.

BacktestOptions, ExchangeOptions and TradeOptions are appsettings configs, in case you ask.

Backtesting subproject:

public class Backtest
{
    private readonly ITradingStrategy _tradingStrategy;
    private readonly IDataProvider _dataProvider;
    private readonly BacktestOptions _backtestOptions;
    
    public Backtest(ITradingStrategy tradingStrategy, IDataProvider dataProvider, BacktestOptions backtestOptions)
    {
        _tradingStrategy = tradingStrategy;
        _dataProvider = dataProvider;
        _backtestOptions = backtestOptions;
    }
    
    public async Task RunAsync()
    {
        ...
    }
}

public interface IDataProvider
{
    Task<List<OHLCV>> DownloadCandlesAsync(string pair, Timeframe timeframe, DateTime startDate, DateTime endDate, int startupCandleCount);
}

public class DataProvider : IDataProvider
{
    private readonly IExchangeClient _exchangeClient;

    public DataProvider(IExchangeClient exchangeClient)
    {
        _exchangeClient = exchangeClient;
    }
    
    public async Task<List<OHLCV>> DownloadCandlesAsync(string pair, Timeframe timeframe, DateTime startDate, DateTime endDate, int startupCandleCount)
    {
        ...
    }
}

public interface IExchangeClient : IDisposable
{
    ...
}

public class BinanceSpotClient : IExchangeClient
{
    private readonly IBinanceClient _client;
    private readonly IBinanceSocketClient _socketClient;
    
    public BinanceSpotClient(ExchangeOptions exchangeOptions)
    {
        _client = new BinanceClient(new BinanceClientOptions()
        {
            ApiCredentials = new ApiCredentials(exchangeOptions.ApiKey, exchangeOptions.SecretKey),
            AutoTimestamp = true,
            AutoTimestampRecalculationInterval = TimeSpan.FromMinutes(30),
            TradeRulesBehaviour = TradeRulesBehaviour.AutoComply,
#if DEBUG
            LogVerbosity = LogVerbosity.Debug
#endif
        });

        _socketClient = new BinanceSocketClient(new BinanceSocketClientOptions()
        {
            ApiCredentials = new ApiCredentials(exchangeOptions.ApiKey, exchangeOptions.SecretKey),
            AutoReconnect = true,
            ReconnectInterval = TimeSpan.FromSeconds(15),
#if DEBUG
            LogVerbosity = LogVerbosity.Debug
#endif
        });
    }
    
    private readonly Subject<IObservable<Unit>> _subject = new Subject<IObservable<Unit>>();
    
    ...
    
    private bool _disposed = false;

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (_disposed)
            return;

        if (disposing)
        {
            if (_client.NotNull())
                _client.Dispose();

            if (_socketClient.NotNull())
            {
                _socketClient.UnsubscribeAll();
                _socketClient.Dispose();
            }

            _subject.OnNext(Observable.Never<Unit>());
        }

        _disposed = true;
    }
}

class Program
{
    static async Task Main(string[] args)
    {
        using IExchangeClient exchangeClient = new BinanceSpotClient(configuration.ExchangeOptions);
        ITradingStrategy tradingStrategy = StrategyUtils.GetStrategyInstance(configuration.BacktestOptions.StrategyName);
        IDataProvider dataProvider = new DataProvider(exchangeClient);
    
        var backtest = new Backtest(tradingStrategy, dataProvider, configuration.BacktestOptions);
        await backtest.RunAsync().ConfigureAwait(false);
        
        Console.ReadLine();
    }
}

Live trading subproject 2:

In this case, I have a factory method implementation, which makes the disposing harder. I'm disposing IExchangeClient in LiveTradeManager and I feel like that's terribly wrong. Maybe I should dispose IExchangeClient elsewhere and leave only the x4 web socket stream subscriptions disposed there?

public static class TradeManagerFactory
{
    public static ITradeManager Build(Manager managers, ExchangeOptions exchangeOptions, TradeOptions tradeOptions)
    {
        IExchangeClient exchangeClient = new BinanceSpotClient(exchangeOptions);
        ITradingStrategy tradingStrategy = StrategyUtils.GetStrategyInstance(tradeOptions.StrategyName);

        ITradeManager tradeManager = null;

        switch (managers)
        {
            case Manager.LiveTradeManager:
                tradeManager = new LiveTradeManager(exchangeClient, tradingStrategy, exchangeOptions, tradeOptions);
                break;
        }

        return tradeManager;
    }
}

public interface ITradeManager : IDisposable
{
    Task RunAsync();
}

public class LiveTradeManager : ITradeManager
{
    private static readonly ILog _logger = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType.Name);

    private readonly IExchangeClient _exchangeClient;
    private readonly ITradingStrategy _tradingStrategy;
    private readonly ExchangeOptions _exchangeOptions;
    private readonly TradeOptions _tradeOptions;

    private readonly Wallets _wallets;
    private readonly Trades _trades;
    private readonly List<OHLCV> _candles;
    
    public LiveTradeManager(IExchangeClient exchangeClient, ITradingStrategy tradingStrategy, ExchangeOptions exchangeOptions, TradeOptions tradeOptions)
    {
        _exchangeClient = exchangeClient;
        _tradingStrategy = tradingStrategy;
        _exchangeOptions = exchangeOptions;
        _tradeOptions = tradeOptions;

        _wallets = new Wallets(exchangeClient);
        _trades = new Trades();
        _candles = new List<OHLCV>();
    }
    
    private CallResult<UpdateSubscription> _tickerSubscription;
    private CallResult<UpdateSubscription> _candlestickSubscription;
    private CallResult<UpdateSubscription> _orderSubscription;
    private IDisposable _throttlerSubscription;
    
    public async Task RunAsync()
    {
        ...
    }
    
    private bool _disposed = false;

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (_disposed)
            return;

        if (disposing)
        {
            if (_exchangeClient.NotNull())
                _exchangeClient.Dispose();

            _exchangeClient.Unsubscribe(_tickerSubscription.Data);
            _exchangeClient.Unsubscribe(_candlestickSubscription.Data);
            _exchangeClient.Unsubscribe(_orderSubscription.Data);
            if (_throttlerSubscription.NotNull())
                _throttlerSubscription.Dispose();
        }

        _disposed = true;
    }
}

class Program
{
    static async Task Main(string[] args)
    {
        var tradeManager = TradeManagerFactory.Build(Manager.LiveTradeManager, configuration.ExchangeOptions, configuration.TradeOptions);
        await tradeManager.RunAsync().ConfigureAwait(false);
        
        Console.ReadLine();
    }
}

Aucun commentaire:

Enregistrer un commentaire