Leveraging hexagonal architecture in Signals Framework’s design

Viktor Borza
Signals Network
Published in
5 min readJun 24, 2019

--

This is a continuation of our article series about the technical aspects of Signals’ platform. Let’s once more dive deep into the fundamentals of a system running many background processes, which allows anyone to simply create, backtest and deploy strategies to enhance the crypto trading experience.

Since we started developing Signals Platform, we have continued to dedicate extra focus to the core component used for defining your trading models — the Signals Framework. Our goal was to make the library so stable that a new version of the library would only need to be released when we extend the API with some features beneficial to clients. We knew we couldn’t force our traders to upgrade their framework version every time we improve the performance of some method or fix some bug. On the other hand, we also wanted the framework to be so flexible, that it would support various data streams and indicator combinations without modification of the library code itself. This article shows how we applied hexagonal architecture principles to solve the described challenges.

Domain Layer

The key to achieving stability and flexibility was to make the Signals Framework library as lightweight as possible. In hexagonal architecture terms, the library basically represents the Signals Network’s domain with models like strategy, data series, market, exchange, currency pair, various types of orders, data marketplace and indicators marketplace. Regarding the domain logic, we have only added methods which are related to building automated trading strategies. There are functions for setting up the strategy data and indicators, registering callbacks executed on data stream updates, entering and exiting market positions, logging, etc. An important part of the domain is also strategy properties like actual market time, current positions and pending orders, which are used for making trading decisions.

Ports and Adapters

You may wonder how we could claim that the library is lightweight when there are methods like EnterLongLimit or properties like Time in the strategy base class. Surely, there must be some code responsible for creating order and publishing it all the way to the UI, as well as some logic behind the Time property, which synchronizes strategy time with all the data streams. You are right, of course, but the actual code executing the logic lives outside of the Signals Framework library code base. In the library, we just depend on ports’ interfaces, as you can see in the code snippet below.

In this preview of strategy base class, IStrategyTimeProvider, IStrategyOrdersManager, and IStrategyLogger are all just ports held in the Signals Framework codebase. The classes implementing the interfaces (the adapters) are located outside of the library, inside the Strategy Execution Service codebase. Basically, we are using Inversion of Control to keep the framework library clean from all the infrastructure dependencies, as the ports are just interfaces declared within the Signals Framework. Adapters are the ones implementing the actual business logic and referencing database drivers, messaging broker client, logging providers, etc.

Data Streams and Indicators Modules

Ports and adapters keep the domain functions inside Signals Framework really small, as they just contain code passing the parameters to the ports. This approach was suitable for methods which are already part of the Signals Framework API. But we needed a different solution to support various subscription models on top of data and indicators marketplaces. We didn’t want to release a new version of the Signals Framework library every time a new technical indicator or a new type of data stream is introduced to the marketplaces.

More importantly, we wanted to avoid dependency from the framework’s package to various data streams and indicators modules. On the contrary, we needed to reference the Signals Framework library from those packages, to use DataSeries, Markets, and other domain models. For that reason, we decided that each indicator and data stream will have its own library, containing its domain models definitions. When the user wants to use it, they must add a reference to the library in the strategy code editor.

In the following code, you can see a hypothetical implementation of a GoogleTrends data stream, which could be used for sentiment analysis on the given market. It contains definitions of GoogleTrend and GoogleTrendOptions domain models. There is also an implementation of DataMarketPlace extension method GoogleTrends(TimeSpan trendStartOffset, string currency), which provides a nice interface for strategy developer to specify the Google Trends stream in one marketPlace.GoogleTrends(5 years, “BTC”) API call.

As you probably noticed, again, there is no implementation of connecting to a real data stream source. The code that depends on third-party providers lies in the data stream driver assembly, which is directly referenced by the Strategy Execution Service. This way, strategies just contain references to data streams’ domain models and we can upgrade the data stream driver implementation any time, without worrying if there is some change which will negatively affect implemented strategies.

Fruits of the design

In the end, the architecture of Signals Framework toolkit has come to look like this.

As you can see, the Signals Framework assembly, data streams assemblies, and indicators assemblies do not depend on any infrastructure components. This keeps them lightweight, without dependencies on other packages, with just domain models code. The data stream drivers and adapters inside the Strategy Execution Service are the ones which cope with the infrastructure code and reference all the modules needed for messaging, database and API handling.

The main benefits of this design are:

  • Upgrading is easy — we can release new versions of the Strategy Execution Service or data stream drivers with no worries about making changes which break the strategies’ code.
  • Less space for bugs — as the packages referenced from the strategies are lightweight and contain almost no logic, there is not much space for coding errors and we don’t have to release new versions just for some bug fixes.
  • Security — we don’t have to worry that someone can explore the logic and infrastructure details by decompiling the framework’s or data streams’ assemblies and reading the code, as they contain only models and methods accessible by the end user anyway.
  • Stability — since the initial release of the Signals Platform, we are still using the same version of Signals Framework, data streams, and indicators assemblies. However, we performed numerous upgrades to the data stream drivers and Signals Framework’s adapters implementations. This way, the end users benefit from improved performance and removed bugs, without any action needed from their side.

Thanks for taking the time to read about another important aspect of our platform. To learn more about the technology behind Signals, take a read of our previous Tech blog, which covers our microservices architecture. If you want to stay updated with the latest development news, be sure to follow us on Twitter or subscribe to our newsletter.

--

--