Indicators

Custom Indicators

Introduction

This page explains how to create and update custom indicators.

Prerequisites

Working knowledge of C#.

If you use Python, you must understand how to work with pandas DataFrames and Series. If you are not familiar with pandas, refer to the pandas documentation.

Get Historical Data

Get some historical market data to warm-up and create a historical record of indicator values. For example, to get data for SPY, run:

var qb = new QuantBook();
var symbol = qb.AddEquity("SPY").Symbol;
var history = qb.History(symbol, 70, Resolution.Daily);
qb = QuantBook()
symbol = qb.AddEquity("SPY").Symbol
history = qb.History(symbol, 70, Resolution.Daily).loc[symbol]

Create Indicator Timeseries

Follow these steps to create an indicator timeseries:

  1. Define a custom indicator class, inherited from the Indicator superclass.
  2. Define a custom indicator class. Note that the PythonIndicator superclass inheritance, Value attribute and Update method is mandatory.
  3. In this tutorial, we're creating an ExpectedShortfallPercent indicator from Monte Carlo method to calculate the expected shortfall in return. We'll be using WindowIndicator superclass instead of Indicator for using a period of historical data stored in a RollingWindow.

    In this tutorial, we're creating an ExpectedShortfallPercent indicator from Monte Carlo method to calculate the expected shortfall in return.

    public class ExpectedShortfallPercent : WindowIndicator<IndicatorDataPoint>, IIndicatorWarmUpPeriodProvider
    {
        private decimal _alpha;
        
        // Set up IndicatorDataPoint attributes for the indicator.
        public IndicatorBase<IndicatorDataPoint> ValueAtRisk { get; }
    
        // Set up the WarmUpPeriod attribute to provide implementation of the IIndicatorWarmUpPeriodProvider interface.
        public override int WarmUpPeriod => Period;
    
        // Set up the constructor.
        // period: The lookback period for return distribution.
        // alpha: Alpha level of VaR cutoff.
        public ExpectedShortfallPercent(int period, decimal alpha)
            : base("ES", period)
        {
            _alpha = alpha;
            ValueAtRisk = new Identity("ES_VaR");
        }
    
        // Override the IsReady method to set up the flag of the Indicator and its IndicatorDataPoint attributes are ready.
        public override bool IsReady => ValueAtRisk.IsReady;
    
        // Mandatory: Override the ComputeNextValue method to calculate the indictor value.
        protected override decimal ComputeNextValue(IReadOnlyWindow<IndicatorDataPoint> window, IndicatorDataPoint input)
        {
            if (Samples < 2)
                return 0m;
                    
            var n = Math.Min(Period, Samples);
            var cutoff = (int) Math.Ceiling(n * _alpha);
            
            var samples = new List<decimal>();
            for (int i = 0; i < window.Count - 1; i++)
            {
                samples.Add( (window[i] - window[i+1]) / window[i+1] );
            }
            var lowest = samples.OrderBy(x => x).Take(cutoff);
            
            ValueAtRisk.Update(input.Time, lowest.Last());
            return lowest.Average();
        }
    }
    class ExpectedShortfallPercent(PythonIndicator):
        import math, numpy as np
        
        def __init__(self, period, alpha):
            self.Value = None   # Attribute represents the indicator value
            self.ValueAtRisk = None
            
            self.alpha = alpha
            
            self.window = RollingWindow[float](period)
        
        # Override the IsReady attribute to flag all attributes values are ready.
        @property
        def IsReady(self) -> bool:
            return self.Value and self.ValueAtRisk
        
        # Method to update the indicator values. Note that it only receives 1 IBaseData object (Tick, TradeBar, QuoteBar) argument.
        def Update(self, input: BaseData) -> bool:
            count = self.window.Count
            
            self.window.Add(input.Close)
            
            # Update the Value and other attributes as the indicator current value.
            if count >= 2:
                cutoff = math.ceil(self.alpha * count)
                
                ret = [ (self.window[i] - self.window[i+1]) / self.window[i+1] for i in range(count-1) ]
                lowest = sorted(ret)[:cutoff]
                
                self.Value = np.mean(lowest)
                self.ValueAtRisk = lowest[-1]
                
            # return a boolean to indicate IsReady.
            return count >= 2
  4. Initialize a new instance of the custom indicator. In this tutorial, 50-period 5% ExpectedShortfallPercent indicator is used.
  5. var es = new ExpectedShortfallPercent(50, 0.05m);
    custom = ExpectedShortfallPercent(50, 0.05)
  6. Create a RollingWindow for each attribute of the indicator to hold their values.
  7. In this example, save 20 data points.

    var time = new RollingWindow<DateTime>(20);
    var window = new Dictionary<string, RollingWindow<decimal>>();
    window["expectedshortfall"] = new RollingWindow<decimal>(20);
    window["valueatrisk"] = new RollingWindow<decimal>(20);
    
    window = {}
    window['time'] = RollingWindow[DateTime](20)
    window['expectedshortfall'] = RollingWindow[float](20)
    window['valueatrisk'] = RollingWindow[float](20)
    
  8. Set handler methods to update the RollingWindows.
  9. es.Updated += (sender, updated) =>
    {
        var indicator = (ExpectedShortfallPercent) sender;
        time.Add(updated.EndTime);
        window["expectedshortfall"].Add(updated);
        window["valueatrisk"].Add(indicator.ValueAtRisk.Current);
    };

    When the indicators receive new data, the handler will add the new IndicatorDataPoints into the RollingWindows.

  10. Iterate the historical market data to update the indicators and the RollingWindows.
  11. foreach(var bar in history){
        es.Update(bar.EndTime, bar.Close);
    }
    for time, row in history.iterrows():
        # The Update method's input must be IBaseData object (Tick, TradeBar, QuoteBar).
        bar = TradeBar(time, symbol, row.open, row.high, row.low, row.close, row.volume)
        custom.Update(bar)
        
        # The Updated event handler is not available for custom indicator in Python, RollingWindows are needed to be updated in here.
        if custom.IsReady:
            window['time'].Add(bar.EndTime)
            window['expectedshortfall'].Add(custom.Value)
            window['valueatrisk'].Add(custom.ValueAtRisk)
  12. Display the data.
  13. Console.WriteLine($"time,{string.Join(',', window.Select(kvp => kvp.Key))}");
    foreach (var i in Enumerable.Range(0, 5).Reverse())
    {
        var data = string.Join(", ", window.Select(kvp => Math.Round(kvp.Value[i],6)));
        Console.WriteLine($"{time[i]:yyyyMMdd}, {data}");
    }
  14. Convert the RollingWindows' data into pandas.DataFrame.
  15. custom_dataframe = pd.DataFrame(window).set_index('time'))

Indicator Helper Method

Jupyter Notebooks don't currently support the qb.Indicator helper method for custom indicator. Please subscribe to this GitHub issue for update.

You can also see our Videos. You can also get in touch with us via Discord.

Did you find this page helpful?

Contribute to the documentation: