7.2.2013

Part 4 - Testing with ddfplus - Reactive benefits with commodity option barrier events

In Part 3 I introduced integrating with Barchart Market Data Solutions real-time market data feeds (ddfplus quote service) for equities, indices, futures and foreign exchange markets. In this post I'll integrate the testing and separation from Part 2.

For reference:

Using a real system means I can introduce separation and testing based on real needs instead of the arbitrary considerations in Part 2. When a ddfplus client connection occurs, a set of what I call "uninitialized" sessions are returned if there's no data for a given symbol in the current session. These quotes will have the symbol and the current session will have zeros for Open/High/Low/Close and a Timestamp of default(DateTime). For my use case of monitoring commodity option barrier events, I don't need to know about these so I want to devise a way to exclude them.

Where to start

I could just write the following code:

private static IObservable<ParsedDdfQuote> CreateQuoteStream(Client client)
{
    return Observable
        .FromEventPattern<Client.NewQuoteEventHandler, Client.NewQuoteEventArgs>(h => client.NewQuote += h, h => client.NewQuote -= h)
        .Select(e => e.EventArgs.Quote)
        .Where(e => e.Sessions["combined"].High > 0)

I could even extract the a method to leave some intent:

.Select(e => e.EventArgs.Quote)
.Where(IsInitialized)

private static bool IsInitialized(Quote quote)
{
    return quote.Sessions["combined"].High > 0;
}

Now I wonder, how do I test this? I could fire up my console example and verify that the uninitialized sessions are filtered out. But what if I can't find any that are uninitialized right now? What if I can but they become initialized when I do my manual inspection?

The reality is I can't control what the market is doing to verify this code works so I need another way. Reproducibility is absolutely central to testing. Maybe this example is simple enough you might choose to just trust it works. I know from experience that a few simple tests can isolate and verify this behavior and they will pay dividends immediately.

How to reliably test this

Naturally I'd like to setup a test case with an uninitialized session, something like:

var uninitializedQuote = new Quote();
uninitializedQuote.Symbol = "CZ2013";
var uninitializedSession = new Session();
uninitializedQuote.Sessions["combined"] = uninitializedSession;
uninitializedSession.High = 0;
...

This is working directly against types in the ddfplus library. There are many reasons this isn't a good idea, chief of which is the above code won't even compile because the external API is encapsulated. Therefore, I want to introduce a layer of separation to inject my testing strategy.

This separation is how I approach working with any external system. Even if the above code were to compile I would still use separation to reduce the surface area of an external system's interaction with my own. Here's why:

  • I'll learn things about the external system that I can make them explicit in this layer of separation.
  • I'll make my assumptions explicit so someone else (my future self included) can challenge them.
  • I want reproducible tests.
  • I want to test my interactions with external systems.
  • I want to simulate external interactions, which is especially valuable with market data where it may take weeks or months for a given condition to occur.
  • I want confidence in what I'm building
  • I want to have a conversation with others and tests are a great place to start.
  • Less so, but a benefit nonetheless - the external API may be updated and I want to minimize the impact.

Unit Testing Projections

The first layer of separation I can introduce is a simple projection from the API type to a type I control. A projection is like an adapter to abstract interactions with a type you don't control with one you do. Once I have this translation tested, I can build further testing on top of it. I'm going to call this projection ParsedDdfQuote.

I'm still stuck with the issue that I can't create the API types. However, in the limited case of testing my projection I am willing to use reflection to get around encapsulation. Here's my first test to map high and low, two things I need to monitor barrier events:

Project high and low

[Test]
public void Create_FromQuote_MapsHighAndLow()
{
    var quote = new Quote();
    var session = new Session();
    quote.AsDynamic().AddSession("combined", session);
    session.AsDynamic().High = 2;
    session.AsDynamic().Low = 1;

    var parsed = new ParsedDdfQuote(quote);

    parsed.High.ShouldBeEquivalentTo(2);
    parsed.Low.ShouldBeEquivalentTo(1);
}

  • Setup
    • Create a quote and a session
    • Add the session to the quote
      • using reflection via AsDynamic, an extension method from ReflectionMagic a package that simplifies using reflection
    • Set the high and low
  • Action
    • create the ParsedDdfQuote from the Quote
  • Assertions
    • high and low are mapped to the parsed projection

Here's the code to make it pass:

public class ParsedDdfQuote
{
    public ParsedDdfQuote(Quote quote)
    {
        var combinedSession = quote.Sessions["combined"];
        High = Convert.ToDecimal(combinedSession.High);
        Low = Convert.ToDecimal(combinedSession.Low);
    }

    public decimal High { get; set; }
    public decimal Low { get; set; }
}

Project symbol

I also need the symbol:

[Test]
public void Create_FromQuote_MapsSymbol()
{
    var quote = new Quote();
    AddCombinedSession(quote);
    quote.AsDynamic().Symbol = "CZ13";

    var parsed = new ParsedDdfQuote(quote);

    parsed.Symbol.ShouldBeEquivalentTo("CZ13");
}

private static Session AddCombinedSession(Quote quote)
{
    var session = new Session();
    quote.AsDynamic().AddSession("combined", session);
    return session;
}

This test is very similar to the last. I extracted AddCombinedSession to share the setup of the combined session.

The code to make it pass:

public class ParsedDdfQuote
{
    public ParsedDdfQuote(Quote quote)
    {
        ...
        Symbol = quote.Symbol;
    }

    ...
    public string Symbol { get; set; }
}

Detect if session is initialized

Finally, I'd like to embed my logic for detecting "uninitialized" sessions into my projection:

[Test]
public void IsInitialized_UninitializedQuote_ReturnsFalse()
{
    var uninitialized = new ParsedDdfQuote {High = 0};

    uninitialized.IsInitialized().Should().BeFalse();
}

  • Setup
    • Notice the explicit assumption about what uninitialized means! High = 0

The code to make it pass:

public class ParsedDdfQuote
{
    ...
    public bool IsInitialized()
    {
        return High > 0;
    }
}

That's it, we now have a simple, tested projection in place!

Inject separation

Now I want to integrate this with my quote stream. I've simply chosen to put this into my ddfplusQuoteSource:

public readonly IObservable<ParsedDdfQuote> QuoteStream;

private static IObservable<ParsedDdfQuote> CreateQuoteStream(Client client)
{
    return Observable
        .FromEventPattern<Client.NewQuoteEventHandler, Client.NewQuoteEventArgs>(h => client.NewQuote += h, h => client.NewQuote -= h)
        .Select(e => e.EventArgs.Quote)
        .Select(q => new ParsedDdfQuote(q));
}

This encapsulates all interactions with the Quote type inside of my ddfplusQuoteSource. Any smoke testing I do of creating this observable would suffice to verify any logic in this integration. I can rely on the Rx framework's testing of Select and my tests of ParsedDdfQuote for the rest.

Testing filtering in the stream

Now that I am working with my own type, I can verify my filtering behaves as I expect. I've decided to implement this filtering as an extension method on IObservable<ParsedDdfQuote>. This way I can compose this into any consumer code. This is my use case:

var source = new ddfplusQuoteSource();
source.QuoteStream
      .ExcludeUninitializedQuotes()
      .Subscribe(PrintQuote);

Now to test this. Filtering is obviously a simple technique, and with my abstracted ParsedDdfQuote.IsInitialized it's even less risky. I will argue at times that testing a well encapsulated method like IsInitialized is sufficient. Nonetheless, I want to demonstrate what will become very useful as we do more advanced things with this stream of quotes. Here's my test method, very similar to Part 2.

[Test]
public void ExcludeUninitializedQuotes()
{
    var uninitializedQuote = new ParsedDdfQuote {High = 0};
    var scheduler = new TestScheduler();
    var quotes = scheduler.CreateHotObservable(ReactiveTest.OnNext(201, uninitializedQuote));

    var onlyInitialized = quotes.ExcludeUninitializedQuotes();
    var quotesObserver = scheduler.Start(() => onlyInitialized); // overload 

    quotesObserver.Messages.Should().BeEmpty();
}

Note: For more details on virtual time scheduling with the reactive framework checkout Testing Rx Queries using Virtual Time Scheduling.

  • Setup
    • Create an uninitializedQuote with my new type ParsedDdfQuote
    • Create a test scheduler and a test observable stream of ParsedDdfQuote
      • Quotes, and other events are "hot" observables in that they fire off information regardless if anyone is subscribed.
      • Schedule my uninitializedQuote to fire at 201 ticks. Ticks are how time is simulated with the Rx-Testing. 201 is significant because by default, calls to TestScheduler.Start subscribe at tick 200.
  • Action
    • filter the quotes with ExcludeUninitializedQuotes
    • simulate and capture quotes into quotesObserver
  • Assertion
    • I shouldn't receive any messages (quotes)

The code to make it pass:

public static IObservable<ParsedDdfQuote> ExcludeUninitializedQuotes(this IObservable<ParsedDdfQuote> quotes)
{
    return quotes
        .Where(q => q.IsInitialized());
}

Conclusion

The reactive framework makes testing and composing interactions with market data streams a breeze. Leveraging the power of LINQ to separate what from how allows us to focus on adding business value, not implementing plumbing. Next, we'll look at more complexity and see how this declarative style of programming really pays off.





comments powered by Disqus

Related Posts