.NET 5 & C#
This page takes you through the steps of coding your first strategy in .NET 5 & C#
In this section we will create a simple EMA based Golden Crossover strategy. Let's start!
Golden Crossover Strategy .NET 5 Sample on GitHub
If you can't wait to see the full code, go straight to the Algorum GitHub and pull the samples repo, which contains the sample strategy explained here. The project to look into in the solution is Algorum.Strategy.GoldenCrossover.
Project Setup
Create a new .NET 5 Console Project in Visual Studio 2019 (Community Edition is Free). Add the following dependencies using NuGet manager
NuGet Package Name | Version |
---|---|
Microsoft.Extensions.Configuration | 5.0+ |
Microsoft.Extensions.Configuration.Binder | 5.0+ |
Microsoft.Extensions.Configuration.CommandLine | 5.0+ |
Microsoft.Extensions.Configuration.EnvironmentVariables | 5.0+ |
Microsoft.Extensions.Configuration.Json | 5.0+ |
Algorum.Quant.Types | 1.0+ |
Strategy Class Setup
Add a new C# class named GoldenCrossoverQuantStrategy with namespace Algorum.Strategy.GoldenCrossover. Derive the GoldenCrossoverQuantStrategy class from Algoru.Quant.QuantEngineClient base class. QuantEngineClient class provides connectivity and API communication with your Algorum Quant Engine running in Algorum Cloud.
namespace Algorum.Strategy
{
/// <summary>
/// Strategy classes should derive from the QuantEngineClient,
/// and implement the abstract methods to receive events like
/// tick data, order update, etc.,
/// </summary>
public class GoldenCrossoverQuantStrategy : QuantEngineClient
{
...
}
}
Strategy Initialization
Write Initialization code for your strategy where you will subscribe for the symbols you want to trade in this strategy, create indicator evaluators for calculating EMA 50 and 200 periods. Ans also define some state that you would be using in the strategy (to track active orders, order direction, etc.,).
namespace Algorum.Strategy.GoldenCrossover
{
/// <summary>
/// Strategy classes should derive from the QuantEngineClient,
/// and implement the abstract methods to receive events like
/// tick data, order update, etc.,
/// </summary>
public class GoldenCrossoverQuantStrategy : QuantEngineClient
{
private class State
{
public bool Bought;
public TickData LastTick;
public TickData CurrentTick;
public List<Order> Orders;
public string CurrentOrderId;
public Order CurrentOrder;
public CrossAbove CrossAboveObj;
}
private const double Capital = 100000;
private const double Leverage = 3; // 3x Leverage on Capital
private Symbol _symbol;
private IIndicatorEvaluator _indicatorEvaluator;
private State _state;
/// <summary>
/// Helps create GoldenCrossoverQuantStrategy class and initialize asynchornously
/// </summary>
/// <param name="url">URL of the Quant Engine Server</param>
/// <param name="apiKey">User Algorum API Key</param>
/// <param name="launchMode">Launch mode of this strategy</param>
/// <param name="sid">Unique Strategy Id</param>
/// <param name="userId">User unique id</param>
/// <returns>Instance of GoldenCrossoverQuantStrategy class</returns>
public static async Task<GoldenCrossoverQuantStrategy> GetInstanceAsync(
string url, string apiKey, StrategyLaunchMode launchMode, string sid, string userId )
{
var strategy = new GoldenCrossoverQuantStrategy( url, apiKey, launchMode, sid, userId );
await strategy.InitializeAsync();
return strategy;
}
private GoldenCrossoverQuantStrategy( string url, string apiKey, StrategyLaunchMode launchMode, string sid, string userId )
: base( url, apiKey, launchMode, sid, userId )
{
// No-Op
}
private async Task InitializeAsync()
{
// Load any saved state
_state = await GetDataAsync<State>( "state" );
if ( ( _state == null ) || ( LaunchMode == StrategyLaunchMode.Backtesting ) )
{
_state = new State();
_state.Orders = new List<Order>();
_state.CrossAboveObj = new CrossAbove();
}
// Create our stock symbol object
// For India users
_symbol = new Symbol() { SymbolType = SymbolType.Stock, Ticker = "TATAMOTORS" };
// For USA users
//_symbol = new Symbol() { SymbolType = SymbolType.Stock, Ticker = "MSFT" };
// Create the technical indicator evaluator that can work with minute candles of the stock
// This will auto sync with the new tick data that would be coming in for this symbol
_indicatorEvaluator = await CreateIndicatorEvaluatorAsync( new CreateIndicatorRequest()
{
Symbol = _symbol,
CandlePeriod = CandlePeriod.Minute,
PeriodSpan = 1
} );
// Subscribe to the symbols we want (one second tick data)
await SubscribeSymbolsAsync( new List<Symbol>
{
_symbol
} );
}
...
}
}
The url parameter is the URL of your Algorum Quant Engine and of the form wss://india-quant-engine-api.algorum.net/quant/engine/api/v1. For local testing and debugging, you will have to point this to the endpoint of the Algorum Quant Engine that was described in the First, understand your Algorum Quant Engine section of this documentation. When you deploy your strategy to Algorum Cloud, this parameter will be automatically filled in with your Algorum Quant Engine endpoint.
The apiKey parameter is your Algorum API Key. For local testing and debugging, you will have to pass this yourself. When you deploy your strategy to Algorum Cloud, this parameter will be automatically filled in with your Algorum API Key.
The launchMode parameter can be any strategy Launch Mode (Backtesting, Paper Trading, Live Trading) that you desire during your local testing and debugging. When you deploy your strategy to Algorum Cloud, this parameter will be automatically filled in with the Launch Mode as specified when running the strategy using Algorum CLI.
The sid parameter is the unique identifier of your strategy. You can pass anything for this during your local testing and debugging. When you deploy your strategy to Algorum Cloud, this parameter will be automatically filled in with the strategy's unique identifier.
The userId parameter is the unique identifier of your Algorum user account. You have to pass your Algorum user account id during your local testing and debugging. You can retrieve this using the user-profile CLI command. When you deploy your strategy to Algorum Cloud, this parameter will be automatically filled in by Algorum Cloud.
Receiving Symbol Data
You have to implement the abstract method OnTickAsync of the QuantEngineClient, where you will receive the tick wise data of the symbols that you have registered to, during the strategy Initialization phase. This method is called both in real time mode (Paper Trading or Live Trading) and also Backtesting mode, so you can use same login to deal with both real time and historical backtesting scenarios.
/// <summary>
/// Called on every tick of the data
/// </summary>
/// <param name="tickData">TickData object</param>
/// <returns>Async Task</returns>
public override async Task OnTickAsync( TickData tickData )
{
...
}
Placing orders
Within the OnTickAsync, if the stock is not in bought state, you will have your Algo logic that retrieves the EMA 50 and 200 candles (each of 5 minutes) and will place orders when EMA 50 crosses above EMA 200. If the stock is bought, then we check if the price of the stock has gone up by 0.25% (take profit condition) or down by 0.5% (stop loss condition), and then place a sell order. For details on the PlaceOrder API you can refer to the API Reference section. Once the order is placed, you will get back an order id that you can use to track the order. At this point the order is NOT completed, it is just accepted by the brokerage or backtesting engine. You will receive order update events as the order passes through different phases until completion, rejection or cancellation.
/// <summary>
/// Called on every tick of the data
/// </summary>
/// <param name="tickData">TickData object</param>
/// <returns>Async Task</returns>
public override async Task OnTickAsync( TickData tickData )
{
_state.CurrentTick = tickData;
// Get the EMA values
var ema50 = await _indicatorEvaluator.EMAAsync( 50 );
var ema200 = await _indicatorEvaluator.EMAAsync( 200 );
if ( ( _state.LastTick == null ) || ( tickData.Timestamp - _state.LastTick.Timestamp ).TotalMinutes >= 1 )
{
await LogAsync( LogLevel.Debug, $"{tickData.Timestamp}, {tickData.LTP}, ema50 {ema50}, ema200 {ema200}" );
_state.LastTick = tickData;
}
if ( ema50 > 0 && ema200 > 0 && _state.CrossAboveObj.Evaluate( ema50, ema200 ) && ( !_state.Bought ) &&
( string.IsNullOrWhiteSpace( _state.CurrentOrderId ) ) )
{
// Place buy order
_state.CurrentOrderId = Guid.NewGuid().ToString();
var qty = Math.Floor( Capital / tickData.LTP ) * Leverage;
await PlaceOrderAsync( new PlaceOrderRequest()
{
OrderType = OrderType.Market,
Price = tickData.LTP,
Quantity = qty,
Symbol = _symbol,
Timestamp = tickData.Timestamp,
TradeExchange = ( LaunchMode == StrategyLaunchMode.Backtesting || LaunchMode == StrategyLaunchMode.PaperTrading ) ? TradeExchange.PAPER : TradeExchange.NSE,
TriggerPrice = tickData.LTP + 0.1,
OrderDirection = OrderDirection.Buy,
Tag = _state.CurrentOrderId
} );
// Store our state
await SetDataAsync( "state", _state );
// Log the buy initiation
var log = $"{tickData.Timestamp}, Placed buy order for {qty} units of {_symbol.Ticker} at price (approx) {tickData.LTP}, {tickData.Timestamp}";
await LogAsync( LogLevel.Information, log );
// DIAG::
Console.WriteLine( log );
}
else if ( _state.CurrentOrder != null )
{
if ( (
( tickData.LTP - _state.CurrentOrder.AveragePrice >= _state.CurrentOrder.AveragePrice * 0.25 / 100 ) ||
( _state.CurrentOrder.AveragePrice - tickData.LTP >= _state.CurrentOrder.AveragePrice * 0.50 / 100 ) )
&&
( _state.Bought ) )
{
await LogAsync( LogLevel.Information, $"OAP {_state.CurrentOrder.AveragePrice}, LTP {tickData.LTP}" );
// Place sell order
_state.CurrentOrderId = Guid.NewGuid().ToString();
var qty = _state.CurrentOrder.FilledQuantity;
await PlaceOrderAsync( new PlaceOrderRequest()
{
OrderType = OrderType.Market,
Price = tickData.LTP,
Quantity = qty,
Symbol = _symbol,
Timestamp = tickData.Timestamp,
TradeExchange = ( LaunchMode == StrategyLaunchMode.Backtesting || LaunchMode == StrategyLaunchMode.PaperTrading ) ? TradeExchange.PAPER : TradeExchange.NSE,
TriggerPrice = tickData.LTP,
OrderDirection = OrderDirection.Sell,
Tag = _state.CurrentOrderId
} );
_state.CurrentOrder = null;
// Store our state
await SetDataAsync( "state", _state );
// Log the sell initiation
var log = $"{tickData.Timestamp}, Placed sell order for {qty} units of {_symbol.Ticker} at price (approx) {tickData.LTP}, {tickData.Timestamp}";
await LogAsync( LogLevel.Information, log );
// DIAG::
Console.WriteLine( log );
}
}
if ( LaunchMode == StrategyLaunchMode.Backtesting )
await SendProgressAsync( tickData );
}
Handling Order Events
Once the order is placed, Algorum Quant Engine will send order events to your strategy, which can be handled in OnOrderUpdateAsync override of your strategy class. This method gets called once for each state change in the Order. This method gets the Order object with the current order status, which you can examine and take appropriate action (mostly setting your state, calculating metrics, etc.,)
/// <summary>
/// Called when there is an update on a order placed by this strategy
/// </summary>
/// <param name="order">Order object</param>
/// <returns>Async Task</returns>
public override async Task OnOrderUpdateAsync( Order order )
{
// Process only orders initiated by this strategy
if ( string.Compare( order.Tag, _state.CurrentOrderId ) == 0 )
{
switch ( order.Status )
{
case OrderStatus.Completed:
lock ( _state )
_state.Orders.Add( order );
if ( order.OrderDirection == OrderDirection.Buy )
{
_state.Bought = true;
_state.CurrentOrder = order;
// Log the buy
var log = $"{order.OrderTimestamp}, Order Id {order.OrderId}, Bought {order.FilledQuantity} units of {order.Symbol.Ticker} at price {order.AveragePrice}";
await LogAsync( LogLevel.Information, log );
// DIAG::
Console.WriteLine( log );
}
else
{
_state.Bought = false;
_state.CurrentOrder = null;
// Log the sell
var log = $"{order.OrderTimestamp}, Order Id {order.OrderId}, Sold {order.FilledQuantity} units of {order.Symbol.Ticker} at price {order.AveragePrice}";
await LogAsync( LogLevel.Information, log );
// DIAG::
Console.WriteLine( log );
}
_state.CurrentOrderId = string.Empty;
var stats = GetStats( _state.CurrentTick );
await SendAsync( "publish_stats", stats );
foreach ( var kvp in stats )
Console.WriteLine( $"{kvp.Key}: {kvp.Value}" );
break;
default:
// Log the order status
{
var log = $"{order.OrderTimestamp}, Order Id {order.OrderId}, Status {order.Status}, Message {order.StatusMessage}";
await LogAsync( LogLevel.Information, log );
// DIAG::
Console.WriteLine( log );
}
break;
}
// Store our state
await SetDataAsync( "state", _state );
}
}
Handling unsolicited errors
Your quant engine may send any errors that it encounters during order processing, tick processing, etc., to your strategy. You can override the QuantEngineClient method OnErrorAsync and custom handle those errors. By default this method in the QuantEngineClient class will simply write the error message to the console.
/// <summary>
/// Called on any unsolicited errors from user's Quant Engine
/// </summary>
/// <param name="errorMsg">Error message</param>
public override async Task OnErrorAsync( string errorMsg )
{
await base.OnErrorAsync( errorMsg );
}
Logging important and debugging messages in your strategy
The LogAsync method of the QuantEngineClient base class will allow you to log important and debugging messages in your strategy. These logs are stored in Algorum Cloud and you can retrieve them by strategy and by date any time you need them. Algorum CLI also lets you see the log stream in real time when your strategy is running live or in backtesting mode, so you can keep an eye on what's going on. The log stream is explained in View real time Log Stream of your strategy section.
// Log the buy
var log = $"{order.OrderTimestamp}, Order Id {order.OrderId}, Bought {order.FilledQuantity} units of {order.Symbol.Ticker} at price {order.AveragePrice}";
await LogAsync( LogLevel.Information, log );
Publishing your strategy Metrics and Progress to Algorum Cloud
You can publish your strategy stats/metrics and progress (applicable in backtesting scenario) to Algorum Cloud. This will enable Algorum CLI show this information to you remotely. In future Algorum Web Apps will use this information to show visual progress and stats/metrics of your strategy.
Below virtual method from our strategy will return the stats, which will then be used by QuantEngineClient base class to send these stats to the Algorum Cloud. As shown in the GetStats method, we are primarily computing P&L and overall portfolio value, and returning this information in a dictionary object. You can add any stats/metrics you want to this dictionary and these will be sent to Algorum Cloud and stored in a cloud storage, and sent to the listeners of your strategy (like Algorum CLI).
public override Dictionary<string, object> GetStats( TickData tickData )
{
var statsMap = new Dictionary<string, object>();
statsMap["Capital"] = Capital;
statsMap["Order Count"] = _state.Orders.Count;
double buyVal = 0;
double sellVal = 0;
double buyQty = 0;
double sellQty = 0;
foreach ( var order in _state.Orders )
{
if ( ( order.Status == OrderStatus.Completed ) && ( order.OrderDirection == OrderDirection.Buy ) && order.Symbol.IsMatch( tickData ) )
{
buyVal += order.FilledQuantity * order.AveragePrice;
buyQty += order.FilledQuantity;
}
if ( ( order.Status == OrderStatus.Completed ) && ( order.OrderDirection == OrderDirection.Sell ) && order.Symbol.IsMatch( tickData ) )
{
sellVal += order.FilledQuantity * order.AveragePrice;
sellQty += order.FilledQuantity;
}
}
if ( sellQty < buyQty )
sellVal += ( buyQty - sellQty ) * tickData.LTP;
double pl = sellVal - buyVal;
statsMap["PL"] = pl;
statsMap["Portfolio Value"] = Capital + pl;
return statsMap;
}
Managing strategy state
You can write your strategy state to the Algorum Cloud using the SetData method. This allows you to save the state in case you have to stop and start your strategy for any reasons like restarting your Algorum Quant Engine for upgrades, Algorum Cloud automatically stopping your strategy at the end of the market hours to optimize the infrastructure cost and efficiently using the resources, etc.,. You can get back your state during the initialization of your strategy using the GetData method.
Loading state
private async Task InitializeAsync()
{
// Load any saved state
_state = await GetDataAsync<State>( "state" );
...
}
Saving state
/// <summary>
/// Called when there is an update on a order placed by this strategy
/// </summary>
/// <param name="order">Order object</param>
/// <returns>Async Task</returns>
public override async Task OnOrderUpdateAsync( Order order )
{
// Process only orders initiated by this strategy
if ( string.Compare( order.Tag, _state.CurrentOrderId ) == 0 )
{
...
// Store our state
await SetDataAsync( "state", _state );
}
}
Proceed to next section to learn how to use our strategy class to backtest locally, and for further steps.
Updated over 2 years ago