Simple crypto momentum trading

I have tried momentum trading out for the past week, with a somewhat positive result. I try to read a lot to get inspired, and by principle I'll try to reach for some of the low hanging fruits there might be. So after reading this blog post: https://medium.com/coinmonks/building-a-crypto-momentum-trading-strategy-4527bbe1bbac (by Fadai Mammadov) I decided to try it out for myself, I wrote the code in C# this time, but would properly migrate it to F# before using in actual production. I'll discuss some pros and cons between C# and F# in a later post. Anyway...

First a quick recap of what momentum trading is, it is a strategy that can work on multiple assets. Different variants of the strategy exists for stocks, FX, crypto and so on. In simple terms, one would buy the assets that have risen most in the last period, and sell assets that have fallen the most. This could seem a bit counterintuitive usually, one might strive to “buy low and sell high” but that would entail figuring out if the current price is indeed low or high. In momentum trading we are more interested in figuring out if there is a trend, if there is (or we think there is..) we would then jump aboard, and try to ride the current trend. 

More deep explanations exist, and I would suggest reading more about the topic. For now the above is all we need, at least with the addition of the next step. 

How do we detect a trend one might ask? There are multiple ways, and I have just scratched the surface. In this example we will try to detect a trend based on the latest performance in the market. As this is crypto we are talking about, that tends to fluctuate a lot. We should try to define a period that is long enough to provide a meaningful trend, but short enough to not miss the trend. How long this period should be, should be figured out through testing, and do keep it mind that it could change. I have chosen 90 days for this example. 

Another thing to consider is false signals,  it can be hard to confirm a trend. In the blog post I refer to in the start, the development in the Bitcoin price is used to verify the overall trend in the market. I have used that method in the example, but I would work on improving it. 

So in this case if the price of BTC is 5% greater than the price today - number of lag days ago, then we continue to check the different pairs, in our chosen “universe”. 

What we want is to try to find the 5 pairs that have risen the most. To do this we have to use some math, as I wrote in the disclaimer, I’m still learning, but luckily ChatGPT can help here. Both with code suggestions, and an explanation of the math and the necessary code. 

What we want to do is to find the annualized slope based on the data provided, for our look back period. To do this we use linear regression like below in C#: 

   public decimal CalculateAnnualizedSlope(Quote[] timeSeriesData)
    	{
        	// Sort the data by date to ensure chronological order
        	var orderedData = timeSeriesData.OrderBy(data => data.Date).ToArray();

        	// Extract the values from the sorted data
        	decimal[] ts = orderedData.Select(data => data.Close).ToArray();

        	// Remove NaN values from the time series
        	ts = ts.Where(x => !DecimalExtensions.IsNaN(x)).ToArray();
        	if (ts.Length == 0)
        	{
            	throw new ArgumentException("Time series contains only NaN values");
        	}

        	// Generate x values (indices)
        	double[] x = Enumerable.Range(0, ts.Length).Select(i => (double)i).ToArray();

        	// Calculate log of the time series using double conversion
        	double[] logTs = ts.Select(v => Math.Log((double)v)).ToArray();

        	// Perform linear regression
        	var ols = SimpleRegression.Fit(x, logTs);

        	decimal slope = (decimal)ols.Item2;
        	decimal intercept = (decimal)ols.Item1;

        	// Calculate r_value (correlation coefficient)
        	double r_value = Correlation.Pearson(x, logTs);

        	// Calculate annualized slope
        	decimal annualizedSlope = (decimal)(Math.Pow(Math.Exp((double)slope), 365) - 1) * 100;

        	// Adjust by r-squared
        	return annualizedSlope * (decimal)Math.Pow(r_value, 2);
    	}

Do note I use the Quote type from the Stock.Indicators library https://github.com/DaveSkender/Stock.Indicators

It is very useful, and I think it is beneficial, to get used to early on to try to model the data we are working on in a common way. That we also easily can use to interoperate with a library used for technical analysis. 

ChatGPTs explanation of the above code is pasted here: 

  1. Generate indices: Establish a simple numerical timeline for the data points.
  2. Log-transform values: Convert exponential growth to linear growth for easier analysis.
  3. Perform linear regression: Fit a linear model to quantify the growth rate.
  4. Extract slope and intercept: Obtain the key parameters of the growth trend.
  5. Calculate correlation coefficient: Assess the fit of the linear model to the data.
  6. Annualize the slope: Convert the growth rate to an annual percentage.
  7. Adjust by r-squared: Account for the model fit quality, providing a more accurate measure of trend strength.

Okay now we have a way to calculate the annualized slope, and the code works on the Quote type from the Stock.Indicators library.

Now we are ready to define the strategy, I have expressed it below in C#

	public IList<QuoteWithSlope> GetMomentumResults(IList<KrakenKlineWithSymbol> klineWithSymbols, int lookback, int lagDays, decimal threshold, DateTime date)
    	{
        	var dataToCheck = klineWithSymbols.Where(x => x.Quote.Date >= date.AddDays(-lookback) && x.Quote.Date <= date); //get quotes within range

        	var groupedData = dataToCheck.GroupBy(k => k.Symbol).ToDictionary(g => g.Key, g => g.Select(k => k.Quote).OrderBy(q => q.Date).ToList());
        	var quotesWithSlope = new List<QuoteWithSlope>();

        	if (groupedData.ContainsKey("XBT/EUR"))
        	{
            	var btcPrices = groupedData["XBT/EUR"]; //get BTC prices for reference

            	foreach (var pair in groupedData)
            	{
                	var quoteClosestToDate = pair.Value.OrderByDescending(x => x.Date).FirstOrDefault(x => x.Date <= date);

                	if (quoteClosestToDate != null)
                	{
                    	var dateForQuote = quoteClosestToDate.Date;

                    	var btcPricesBeforeAndOnDate = btcPrices.Where(x => x.Date <= dateForQuote).OrderByDescending(x => x.Date);

                    	//check if above threshold

                    	var btcPriceNearestDate = btcPricesBeforeAndOnDate.FirstOrDefault();
                    	if (btcPriceNearestDate != null)
                    	{
                        	var btcPriceAtLagDate = btcPricesBeforeAndOnDate.FirstOrDefault(x => x.Date <= btcPriceNearestDate.Date.AddDays(-lagDays));

                        	if (btcPriceAtLagDate != null)
                        	{
                            	var percentageChange = (btcPriceNearestDate.Close / btcPriceAtLagDate.Close - 1);

                            	if (percentageChange > threshold)
                            	{
                                	//btc price change is positive for period. we can trade
                                	var quotesToCheck = groupedData[pair.Key].Where(x => x.Date <= dateForQuote).ToArray();
                                	var slope = quoteHelper.CalculateAnnualizedSlope(quotesToCheck);

                                	var quoteWithSlope = new QuoteWithSlope { Quote = quoteClosestToDate, Slope = slope, Symbol = pair.Key };
                                	quotesWithSlope.Add(quoteWithSlope);
                            	}
                            	else
                            	{
                                	//no trade..
                            	}

                        	}
                    	}
                	}

            	}
        	}

        	//get five largest
        	var largest = quotesWithSlope.OrderByDescending(x => x.Slope).Take(5);
        	return largest.ToList();
    	}

Do note that I have defined two types QuoteWithSlope and KrakenKlineWithSymbol. They look like this in C#:

    public class QuoteWithSlope
    {
        public Quote Quote { get; set; }
        public string Symbol { get; set; }
        public decimal Slope { get; set; }
    }

    public class KrakenKlineWithSymbol
    {
        public Quote Quote { get; set; }
        public string Symbol { get; set; }
    }

    

They are pretty similar, and I properly should combine them, but the important thing is mostly the KrakenKlineWithSymbol. This is the actual data from Kraken.
In my case, I have stored the OHLC data in my QuestDB instance (https://questdb.io/ a topic for another post) but to follow this example you should get the data from Kraken somehow.

The Kraken API documentation can be found here: https://docs.kraken.com/rest/#tag/Spot-Market-Data/operation/getOHLCData

As I mentioned at the very start I have tried this strategy out this week, and I plan to do a rebalancing of the pairs that the strategy suggested. (Do note I strongly suggest you to do extensive backtesting of all your strategies)

At the time or writing the strategy performs.. well it earned a bit. The strategy suggested, I bought these five pairs:

TRU/EUR
MNGO/EUR
FARM/EUR
PEPE/EUR
ONDO/EUR

At a total purchase price of: 148,34 EUR, the current value: 148,95 EUR. This means a positive return of 0.411%. Not amazing but let's wait and see until the positions are closed.

That was it for now.

This is not financial advice, do your own research.. and also have fun.

Simon

Github repo with code: https://github.com/SimonRiis/simple-crypto-momentum-trading