.NET 5 and C#. Glimpse of creating a quant strategy in Algorum

No crap! Just a piece of code to create a simple RSI based quant strategy in Algorum using .NET 5 and C#. You get technical indicator evaluation, logging, state management, real time data ticks, order status handling, all in just few lines of code. And your strategy code is abstracted from underlying broker API, and runs both in paper trading (virtual money) mode and actual broker live mode. Enjoy this for now!


using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Algorum.Quant.Types;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;

namespace Algorum.Strategy.SupportResistance
{
   /// 
   /// Strategy classes should derive from the QuantEngineClient, 
   /// and implement the abstract methods to receive events like 
   /// tick data, order update, etc.,
   /// 
   public class RSIStrategy : QuantEngineClient
   {
      private class State
      {
         public bool Bought;
         public TickData LastTick;
         public TickData CurrentTick;
         public List Orders;
         public string CurrentOrderId;
         public Order CurrentOrder;
         public CrossBelow CrossBelowObj;
         public CrossAbove CrossAboveObj;
         public bool DayChanged;
      }

      public const double Capital = 1000000;
      private const double Leverage = 8; // 8x Leverage on margin by Brokerage

      private Symbol _symbol;
      private IIndicatorEvaluator _indicatorEvaluator;
      private State _state;

      /// 
      /// Helps create strategy class object class and initialize asynchornously
      /// 
      /// URL of the Quant Engine Server
      /// User Algorum API Key
      /// Launch mode of this strategy
      /// Unique Strategy Id
      /// User unique id
      /// Instance of RSIStrategy class
      public static async Task GetInstanceAsync(
         string url, string apiKey, StrategyLaunchMode launchMode, string sid, string userId )
      {
         var strategy = new RSIStrategy( url, apiKey, launchMode, sid, userId );
         await strategy.InitializeAsync();
         return strategy;
      }

      private RSIStrategy( 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" );

         if ( ( _state == null ) || ( LaunchMode == StrategyLaunchMode.Backtesting ) )
         {
            _state = new State();
            _state.Orders = new List();
            _state.CrossBelowObj = new CrossBelow();
            _state.CrossAboveObj = new CrossAbove();
            _state.DayChanged = false;
         }

         // Create our stock symbol object
         // For India users
         _symbol = new Symbol() { SymbolType = SymbolType.FuturesIndex, Ticker = "NIFTY" };

         // For USA users
         //_symbol = new Symbol() { SymbolType = SymbolType.Stock, Ticker = "AAPL" };

         // 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 = 60
         } );

         // Subscribe to the symbols we want (one second tick data)
         await SubscribeSymbolsAsync( new List
         {
            _symbol
         } );
      }

      /// 
      /// Called when there is an update on a order placed by this strategy
      /// 
      /// Order object
      /// Async Task
      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 );
         }
      }

      /// 
      /// Called on every tick of the data
      /// 
      /// TickData object
      /// Async Task
      public override async Task OnTickAsync( TickData tickData )
      {
         try
         {
            var prevTick = _state.CurrentTick;
            _state.CurrentTick = tickData;

            if ( prevTick == null || ( !_state.DayChanged && tickData.Timestamp.Day > prevTick.Timestamp.Day && !_state.Bought ) )
            {
               _state.DayChanged = true;
            }

            // Get the RSI value
            var rsi = await _indicatorEvaluator.RSIAsync( 14 );
            var (direction, strength) = await _indicatorEvaluator.TRENDAsync( 14 );

            if ( ( _state.LastTick == null ) || ( tickData.Timestamp - _state.LastTick.Timestamp ).TotalMinutes >= 1 )
            {
               await LogAsync( LogLevel.Debug, $"{tickData.Timestamp}, {tickData.LTP}, rsi {rsi}, direction {direction}, strength {strength}" );
               _state.LastTick = tickData;
            }

            // We BUY the stock when the RSI value crosses below 30 (oversold condition)
            if (
               rsi > 0 && _state.CrossBelowObj.Evaluate( rsi, 15 ) &&
               direction == 2 && strength >= 10 &&
               ( !_state.Bought ) && ( string.IsNullOrWhiteSpace( _state.CurrentOrderId ) ) )
            {
               _state.DayChanged = false;

               // 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,
                  OrderDirection = OrderDirection.Buy,
                  SlippageType = SlippageType.TIME,
                  Slippage = 1000,
                  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.1 / 100 ) ||
                     tickData.Timestamp.Hour >= 15 )
                     &&
                  ( _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,
                     SlippageType = SlippageType.TIME,
                     Slippage = 1000,
                     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 );
               }
            }
         }
         catch ( Exception ex )
         {
            await LogAsync( LogLevel.Error, ex.ToString() );

            // DIAG::
            Console.WriteLine( ex );
         }
         finally
         {
            await SendProgressAsync( tickData );
         }
      }

      /// 
      /// Called on custom events passed on by users to the strategy
      /// 
      /// Custom event data string
      /// Async Task
      public override async Task OnCustomEventAsync( string eventData )
      {
         await LogAsync( LogLevel.Information, eventData );
         return string.Empty;
      }

      /// 
      /// Start trading
      /// 
      /// TradingRequest object
      /// 
      public override async Task StartTradingAsync( TradingRequest tradingRequest )
      {
         // Preload candles
         await _indicatorEvaluator.PreloadCandlesAsync( 210, DateTime.UtcNow, tradingRequest.ApiKey, tradingRequest.ApiSecretKey );

         await base.StartTradingAsync( tradingRequest );
      }

      /// 
      /// Backtest this strategy
      /// 
      /// BacktestRequest object
      /// Backtest id
      public override async Task BacktestAsync( BacktestRequest backtestRequest )
      {
         // Preload candles
         await _indicatorEvaluator.PreloadCandlesAsync( 20, backtestRequest.StartDate.AddDays( 1 ), backtestRequest.ApiKey, backtestRequest.ApiSecretKey );

         // Run backtest
         return await base.BacktestAsync( backtestRequest );
      }

      public override Dictionary GetStats( TickData tickData )
      {
         var statsMap = new Dictionary();

         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;
      }
   }
}

using System;
using System.Threading.Tasks;
using Algorum.Quant.Types;
using Microsoft.Extensions.Configuration;

namespace Algorum.Strategy.SupportResistance
{
   class Program
   {
      static async Task Main( string[] args )
      {
         // Get the url to connect from the arguments.
         // This will be local url when deployed on Algorum cloud.
         // For local debugging and testing, you can connect to the remote Algorum cloud, without deploying.
         var configBuilder = new ConfigurationBuilder().AddJsonFile( "appsettings.json" ).AddEnvironmentVariables();
         var config = configBuilder.Build();

         string url = config.GetValue( "url" );
         string apiKey = config.GetValue( "apiKey" );
         string userId = config.GetValue( "userId" );
         string sid = config.GetValue( "sid" );
         string bkApiKey = config.GetValue( "bkApiKey" );
         string bkApiSecretKey = config.GetValue( "bkApiSecretKey" );
         string clientCode = config.GetValue( "clientCode" );
         string password = config.GetValue( "password" );
         string twoFactorAuth = config.GetValue( "twoFactorAuth" );
         BrokeragePlatform brokeragePlatform = config.GetValue( "brokeragePlatform" );
         int samplingTime = config.GetValue( "samplingTime" );
         StrategyLaunchMode launchMode = config.GetValue( "launchMode" );

         url += $"?sid={sid}&apiKey={apiKey}&launchMode={launchMode}";

         // Create our strategy object
         var strategy = await RSIStrategy.GetInstanceAsync( url, apiKey, launchMode, sid, userId );

         // If we are started in backtestign mode, start the backtest
         if ( launchMode == StrategyLaunchMode.Backtesting )
         {
            DateTime startDate = DateTime.ParseExact( config.GetValue( "startDate" ), "dd-MM-yyyy", null );
            DateTime endDate = DateTime.ParseExact( config.GetValue( "endDate" ), "dd-MM-yyyy", null );

            await strategy.BacktestAsync( new BacktestRequest()
            {
               StartDate = startDate,
               EndDate = endDate,
               ApiKey = bkApiKey,
               ApiSecretKey = bkApiSecretKey,
               SamplingTimeInSeconds = samplingTime,
               BrokeragePlatform = brokeragePlatform,
               Capital = RSIStrategy.Capital
            } );
         }
         else
         {
            // Else, start trading in live or paper trading mode as per the given launchMode
            await strategy.StartTradingAsync( new TradingRequest()
            {
               ApiKey = bkApiKey,
               ApiSecretKey = bkApiSecretKey,
               ClientCode = clientCode,
               Password = password,
               TwoFactorAuth = twoFactorAuth,
               SamplingTimeInSeconds = samplingTime,
               BrokeragePlatform = brokeragePlatform,
               Capital = RSIStrategy.Capital
            } );
         }

         // Wait until our strategy is stopped
         strategy.Wait();
      }
   }
}

Leave a Reply