Skip to main content
3 of 3
added 2630 characters in body
Peter Csala
  • 9.8k
  • 1
  • 15
  • 33

Even though you have not explicitly mentioned it in your question but I assume you are using this library.

Your SubscribeToAsync is a good starting point. But your to be provided func parameters are still have much resemblance as you can see

await SubscribeToAsync(() => _socketClient.Streams.SubscribeToOrderUpdatesAsync(data =>
{
    _logger.LogInformation("Orders: {@Data}", data.Data);
}, cancellationToken));

await SubscribeToAsync(() => _socketClient.Streams.SubscribeToUserTradeUpdatesAsync(data =>
{
    _logger.LogInformation("Fills: {@Data}", data.Data);
}, cancellationToken));

If you want to move as much logic as possible into your SubscribeToAsync then you have to assess the above method calls. What are the differences?

  • SubscribeToOrderUpdatesAsync vs SubscribeToUserTradeUpdatesAsync
  • "Orders: vs "Fills:

If you could provide these information to the SubscribeToAsync methods then you could get rid of the SubscribeToOrdersAsync and SubscribeToFillsAsync methods entirely.


Fortunately you can do that:

private async Task<UpdateSubscription?> SubscribeToAsync<T>(
    Func<IFTXSocketClientStreams, Func<Action<DataEvent<T>>, CancellationToken, Task<CallResult<UpdateSubscription>>>> methodSelector,
    string logPrefix, CancellationToken token)
{
    var asyncMethod = methodSelector(_socketClient.Streams);
    var subscription = await asyncMethod(data => _logger.LogInformation(logPrefix +": {@Data}", data.Data), token);

    if (!subscription.Success)
        return null;

    subscription.Data.ConnectionLost += () => _logger.LogError("Connection lost");
    subscription.Data.ConnectionRestored += _ => _logger.LogInformation("Connection restored");

    return subscription.Data;
}
  • The methodSelector parameter: It select the to be called method from the IFTXSocketClientStreams inteface
    • Since FTXOrder and FTXUserTrade don't have a common base class that's why you have to make the SubscribeToAsync generic
  • The logPrefix parameter: The prefix of the logging message
  • The token parameter: The cancellation token

With this in your hand the entire class could be shortened like this:

public class FtxProducer : IHostedService
{
    private readonly ILogger<FtxProducer> _logger;
    private readonly IFTXSocketClient _socketClient;

    public FtxProducer(ILogger<FtxProducer> logger, IFTXSocketClient socketClient)
        => (_logger, _socketClient) = (logger, socketClient);

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        await SubscribeToAsync<FTXOrder>(stream => stream.SubscribeToOrderUpdatesAsync, "Orders", cancellationToken);
        await SubscribeToAsync<FTXUserTrade>(stream => stream.SubscribeToUserTradeUpdatesAsync, "Fills", cancellationToken);
    }

    public Task StopAsync(CancellationToken cancellationToken)
        => _socketClient.UnsubscribeAllAsync();

    private async Task<UpdateSubscription?> SubscribeToAsync<T>(
        Func<IFTXSocketClientStreams, Func<Action<DataEvent<T>>, CancellationToken, Task<CallResult<UpdateSubscription>>>> methodSelector,
        string logPrefix, CancellationToken token)
    {
        var asyncMethod = methodSelector(_socketClient.Streams);
        var subscription = await asyncMethod(data => _logger.LogInformation(logPrefix +": {@Data}", data.Data), token);

        if (!subscription.Success) return null;

        subscription.Data.ConnectionLost += () => _logger.LogError("Connection lost");
        subscription.Data.ConnectionRestored += _ => _logger.LogInformation("Connection restored");

        return subscription.Data;
    }
}

UPDATE #1

May you just change _logger.LogInformation(logPrefix +": {@Data}", data.Data) to something more generic?

private async Task<UpdateSubscription?> SubscribeToWithLoggingAsync<T>(
    Func<IFTXSocketClientStreams, Func<Action<DataEvent<T>>, CancellationToken, Task<CallResult<UpdateSubscription>>>> methodSelector,
    string logPrefix, CancellationToken token)
    => await SubscribeToAsync(methodSelector, data => _logger.LogInformation(logPrefix +": {@Data}", data.Data), token);

private async Task<UpdateSubscription?> SubscribeToAsync<T>(
    Func<IFTXSocketClientStreams, Func<Action<DataEvent<T>>, CancellationToken, Task<CallResult<UpdateSubscription>>>> methodSelector,
    Action<DataEvent<T>> handler, CancellationToken token)
{
    var asyncMethod = methodSelector(_socketClient.Streams);
    var subscription = await asyncMethod(handler, token);

    if (!subscription.Success) return null;

    subscription.Data.ConnectionLost += () => _logger.LogError("Connection lost");
    subscription.Data.ConnectionRestored += _ => _logger.LogInformation("Connection restored");

    return subscription.Data;
}
  • The SubscribeToWithLoggingAsync is a specialized version of the more generic SubscribeToAsync
    • It specifies that the handler should perform some simple logging
  • The SubscribeToAsync method's signature looks a bit terrifying now, but it is generic enough to be reused in multiple ways

UPDATE #2

public abstract Task<CallResult<UpdateSubscription>> SubscribeToTickerUpdatesAsync(string symbol, Action<DataEvent<FTXStreamTicker>> handler, CancellationToken ct = default(CancellationToken)) I wasn't able to call this method.

If you look at the signature of the method then you can see there is an extra input parameter called symbol. The SubscribeToAsync's methodSelector is unaware of this.

In order to support the call of SubscribeToTickerUpdatesAsync or SubscribeToTradeUpdatesAsync like this:

await SubscribeWithSymbolToAsync<FTXStreamTicker>(stream => stream.SubscribeToTickerUpdatesAsync, "Ticker", cancellationToken);
await SubscribeWithSymbolToAsync<IEnumerable<FTXTrade>>(stream => stream.SubscribeToTradeUpdatesAsync, "Trade", cancellationToken);

we need the following trick:

private async Task<UpdateSubscription?> SubscribeWithSymbolToAsync<T>(
    Func<IFTXSocketClientStreams, Func<string, Action<DataEvent<T>>, CancellationToken, Task<CallResult<UpdateSubscription>>>> methodSelector,
    string symbol, CancellationToken token)
{
    var asyncMethod = (Action<DataEvent<T>> handler, CancellationToken cToken) =>
        methodSelector(_socketClient.Streams)(symbol, handler, cToken);

    var subscription = await asyncMethod(data => _logger.LogInformation(symbol + ": {@Data}", data.Data), token);

    return SubscribeToEvents(subscription);
}

private async Task<UpdateSubscription?> SubscribeToAsync<T>(
    Func<IFTXSocketClientStreams, Func<Action<DataEvent<T>>, CancellationToken, Task<CallResult<UpdateSubscription>>>> methodSelector,
    Action<DataEvent<T>> handler, CancellationToken token)
{
    var asyncMethod = methodSelector(_socketClient.Streams);
    var subscription = await asyncMethod(handler, token);

    return SubscribeToEvents(subscription);
}

private UpdateSubscription SubscribeToEvents(CallResult<UpdateSubscription> subscription)
{
    if (!subscription.Success) return null;

    subscription.Data.ConnectionLost += () => _logger.LogError("Connection lost");
    subscription.Data.ConnectionRestored += _ => _logger.LogInformation("Connection restored");

    return subscription.Data;
}
  • The SubscribeWithSymbolToAsync binds the symbol formal parameter of the methodSelector to the provided symbol parameter
    • In other words, we convert a Func<T1, T2, T3> to a Func<T2, T3> by supplying T1
  • The SubscribeWithSymbolToAsync does not rely on SubscribeToAsync so I extract the common part
    • The SubscribeToEvents handles the event subscribtions
Peter Csala
  • 9.8k
  • 1
  • 15
  • 33