'Convert RSI from pinescript to C#?

From Trading View, we see that the rsi can be written in pinescript as follows:

pine_rsi(x, y) => 
    u = max(x - x[1], 0) // upward change
    d = max(x[1] - x, 0) // downward change
    rs = rma(u, y) / rma(d, y)
    rsi = 100 - 100 / (1 + rs)

I've rewritten this in C#:

public static double RelativeStrengthIndex(List<double> input, int samples)
{
    List<double> gains = new List<double>();
    List<double> losses = new List<double>();

    for (int i = input.Count - samples; i < input.Count; i++)
    {
        double change = input[i] - input[i - 1];
        gains.Add(change >= 0 ? change : 0);
        losses.Add(change < 0 ? -1 * change : 0);
    }

    double rs = RollingMovingAverage(gains, samples) / RollingMovingAverage(losses, samples);
    return 100 - 100 / (1 + rs);
}

However, my results are not equivalent. They are different enough to rule out differences in data (data from trading view and other providers can be slightly different). I've been trying to fix this for a while and have completely given up.

Does anyone know why my code produces different results?



Solution 1:[1]

I had exactly the same problem and discovered that the RSI is based on a Rolling Moving Average (RMA) which is a cumulative function. For a 14-period RSI you need about 100 bars for this to become stable. I had to put values into an excel spreadsheet to figure this out and get a formula that matched Pinescript.

I ended up writing my own C# RSI function below based on this and tested and it gets same results as pinescript. The base class AnalyzableBase is from Trady - an open source indicator framework.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using HodlBot.Common.Extensions;
using Trady.Analysis;
using Trady.Analysis.Infrastructure;
using Trady.Core.Infrastructure;

namespace HodlBot.Lite.Strategy
{
    public class FastRsi<TOutput> : AnalyzableBase<IOhlcv, IOhlcv, decimal?, TOutput>
    {
        private readonly List<IOhlcv> _inputs;
        public int Period { get; }
        private List<decimal?> _rsi = new List<decimal?>();
        private int _periodMinus1;
        private List<DateTimeOffset> _dateTimes;
        private decimal _lastGain = 0;
        private decimal _lastLoss = 0;

        public FastRsi(IEnumerable<IOhlcv> inputs, int period) : base(inputs, i => i)
        {
            _inputs = inputs.ToList();
            _dateTimes = _inputs.Select(x => x.DateTime).ToList();
            Period = period;
            _rsi.Add(null);
            _periodMinus1 = period - 1;

            // RMA_Gain=((Gain*(Period-1)) + RMA_Gain[i-1])/Period
            for (int i = 1; i < _inputs.Count; i++)
            {
                decimal change = _inputs[i].Close - _inputs[i-1].Close;
                decimal gain = change > 0 ? change : 0;
                decimal loss = change < 0 ? -change : 0;
                decimal rmaGain = ((_lastGain * _periodMinus1) + gain) / period;
                decimal rmaLoss = ((_lastLoss * _periodMinus1) + loss) / period;
                decimal rs = rmaLoss == 0 ? 100 : rmaGain / rmaLoss;
                decimal rsi = 100 - (100 / (1 + rs));
                _rsi.Add(i < period ? null : (decimal?)rsi);

                _lastGain = rmaGain;
                _lastLoss = rmaLoss;
            }
        }

        public FastRsi<TOutput> AddOhlcv(IOhlcv ohlc)
        {
            _inputs.Add(ohlc);
            _dateTimes.Add(ohlc.DateTime);
            IReadOnlyList<IOhlcv> mappedInputs = _inputs;
            IReadOnlyList<DateTimeOffset> mappedDateTimes = _dateTimes;
            
            // Trady base class needs these to be able to compute a single index. 
            // TODO: Set these 
            typeof(FastRsi)
                .GetField("_mappedInputs", BindingFlags.Instance | BindingFlags.NonPublic)
                .SetValue(this, mappedInputs);

            typeof(AnalyzableBase<IOhlcv, IOhlcv, decimal?, TOutput>)
                .GetField("_mappedDateTimes", BindingFlags.Instance | BindingFlags.NonPublic)
                .SetValue(this, mappedDateTimes);

            int i = _mappedInputs.Count - 1;
            decimal change = i > 0 ? _mappedInputs[i].Close - _mappedInputs[i - 1].Close : 0;
            decimal gain = change > 0 ? change : 0;
            decimal loss = change < 0 ? -change : 0;
            decimal rmaGain = ((_lastGain * _periodMinus1) + gain) / Period;
            decimal rmaLoss = ((_lastLoss * _periodMinus1) + loss) / Period;
            decimal rs = rmaLoss == 0 ? 100 : rmaGain / rmaLoss;
            decimal rsi = 100 - (100 / (1 + rs));
            _rsi.Add(i < Period ? null : (decimal?)rsi);

            _lastGain = rmaGain;
            _lastLoss = rmaLoss;

            return this;
        }

        protected override decimal? ComputeByIndexImpl(IReadOnlyList<IOhlcv> mappedInputs, int index)
        {
            return _rsi[index];
        }
    }

    public class FastRsi : FastRsi<AnalyzableTick<decimal?>>
    {
        public FastRsi(IEnumerable<IOhlcv> inputs, int period) : base(inputs, period)
        {
        }
    }
}

There's a bit more on my implementation and reasoning here.

Solution 2:[2]

You are not handling the initial MA period, which you need to take into account for the first RSI value.

Following method I have written for both RSI and StochRSI using the historical data for given stock symbol

public DataTable getRSIDataTableFromDaily(string symbol, string exchange, string seriestype = "CLOSE", string outputsize = "Compact", string time_interval = "1d",
            string fromDate = null, string period = "14", bool stochRSI = false)
        {
            DataTable dailyTable = null;
            //DataTable rsiDataTable = null;
            int iPeriod;
            double change, gain, loss, avgGain = 0.00, avgLoss = 0.00, rs, rsi;
            double sumOfGain = 0.00, sumOfLoss = 0.00;
            //DateTime dateCurrentRow = DateTime.Today;
            List<string> seriesNameList;
            try
            {
            //GetStockPriceData returns data table with historical OHLC values for period specified
                dailyTable = GetStockPriceData(symbol, exchange, seriestype, outputsize, time_interval, fromDate, sqlite_cmd: null);
                if ((dailyTable != null) && (dailyTable.Rows.Count > 0))
                {
                    iPeriod = System.Convert.ToInt32(period);
                    DataColumn newCol;
                    if (stochRSI == false)
                    {
                    //If caller wants only RSI then we will only use the seriestype specified by caller to calculate the RSI for specified period
                        newCol = new DataColumn("RSI_" + seriestype, typeof(decimal));
                        newCol.DefaultValue = 0.00;

                        dailyTable.Columns.Add(newCol);
                        seriesNameList = new List<string> { seriestype };
                    }
                    else
                    {
                    //If caller wants StochRSI then we need to include all of OHLC values
                        newCol = new DataColumn("RSI_OPEN", typeof(decimal));
                        newCol.DefaultValue = 0.00;

                        dailyTable.Columns.Add(newCol);
                        newCol = new DataColumn("RSI_CLOSE", typeof(decimal));
                        newCol.DefaultValue = 0.00;

                        dailyTable.Columns.Add(newCol);
                        newCol = new DataColumn("RSI_HIGH", typeof(decimal));
                        newCol.DefaultValue = 0.00;

                        dailyTable.Columns.Add(newCol);
                        newCol = new DataColumn("RSI_LOW", typeof(decimal));
                        newCol.DefaultValue = 0.00;

                        dailyTable.Columns.Add(newCol);

                        seriesNameList = new List<string> { "CLOSE", "OPEN", "HIGH", "LOW" };
                    }
                    foreach (var item in seriesNameList)
                    {
                        change = gain = loss = avgGain = avgLoss = rs = rsi = 0.00;
                        sumOfGain = sumOfLoss = 0.00;

                        for (int rownum = 1; rownum < dailyTable.Rows.Count; rownum++)
                        {
                            //current - prev
                            //change = System.Convert.ToDouble(dailyTable.Rows[rownum][seriestype]) - System.Convert.ToDouble(dailyTable.Rows[rownum - 1][seriestype]);
                            change = System.Convert.ToDouble(dailyTable.Rows[rownum][item.ToString()]) - System.Convert.ToDouble(dailyTable.Rows[rownum - 1][item.ToString()]);
                            //dateCurrentRow = System.Convert.ToDateTime(dailyTable.Rows[rownum]["TIMESTAMP"]);

                            if (change < 0)
                            {
                                loss = Math.Abs(change);
                                gain = 0.00;
                            }
                            else
                            {
                                gain = change;
                                loss = 0.00;
                            }

                            //for the first iPeriod keep adding loss & gain
                            if (rownum < iPeriod)
                            {
                                sumOfGain += gain;
                                sumOfLoss += loss;
                            }
                            else
                            {
                                if (rownum == iPeriod)
                                {
                                //this means we are at period specified
                                    sumOfGain += gain;
                                    sumOfLoss += loss;
                                    //we also find  other fields and SAVE
                                    avgGain = sumOfGain / iPeriod;
                                    avgLoss = sumOfLoss / iPeriod;
                                    rs = avgGain / avgLoss;
                                    rsi = 100 - (100 / (1 + rs));
                                }
                                else
                                {
                                //this means we are now beyond the period, calculate RSI
                                    avgGain = ((avgGain * (iPeriod - 1)) + gain) / iPeriod;
                                    avgLoss = ((avgLoss * (iPeriod - 1)) + loss) / iPeriod;
                                    rs = avgGain / avgLoss;
                                    rsi = 100 - (100 / (1 + rs));
                                }
                                //dailyTable.Rows[rownum]["RSI"] = Math.Round(rsi, 2);
                                dailyTable.Rows[rownum]["RSI_" + item.ToString()] = Math.Round(rsi, 2);
                            }
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("getRSIDataTableFromDaily exception: " + ex.Message);

                if (dailyTable != null)
                {
                    dailyTable.Clear();
                    dailyTable.Dispose();
                }
                dailyTable = null;
            }
            return dailyTable;
        }

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Dr. Andrew Burnett-Thompson
Solution 2 Vinay