Robert's Homepage

Strategy Pattern and Service Objects in Ruby

#ruby #software development #software-design-patterns

The following is an excerpt from my book Building and Deploying Crypto Trading Bots

A common design pattern in software development is known as the “Strategy” pattern. The pattern proposes that when you have a scenario with multiple execution paths, each path be considered as a “strategy” object (or function in functional languages) that contains the algorithm implementation to execute. The benefit of doing this two fold. First, it allows the algorithm to vary from the clients that uses it. Second, it liberates instances of logic (“strategies”) from individual client implementations allow for higher reuse within the software.

To provide a more concrete example we use the following example with about trading:

 # Abstract base class
 class Strategy
	def initialize
		raise "abstract"
	end
	
	def exec
	 raise "abstract"
	end
 end

We define an abstract “Strategy” class which our strategies will inherit from. Our strategy objects in this example will encapsulate the algorithms necessary to calculate the “Simple Moving Average” for a particular data set. Calculating simple moving average is a technique used by technical trading analysts to determine the trend direction of a particular market (for example a stock). In our case we will choose arbitrary calculations for twelve and twenty-four day moving average, the implementation is not important here:

 class TwelveDayMovingAverage < Strategy
	attr_accessor :data
	def initialize(data)
		@data = data
	end

	def exec
		# data analysis and return true or false result
	end
 end

 class TwentyFourDayMovingAverage < Strategy
	attr_accessor :data
	def initialize(data)
		@data = data
	end

	def exec
		# data analysis and return 'up' or 'down'
	end
 end

We create two concrete strategy objects TwelveDayMovingAverage and TwentyFourDayMovingAverage that inherit from the base Strategy class and override the interface’s exec method with their own algorithm to return a result. Finally we have the context under which the strategies will be called:

 class TradingBot
	 attr_reader :trade_strategy
	 def initialize(trade_strategy)
		 @trade_strategy = trade_strategy
	 end
	 
	 def trend?(data)
		case strategy
		 when :twelve_day
			TwelveDayMovingAverage.new(data).exec
		 when :twenty_four_day
			TwentyFourDayMovingAverage.new(data).exec
		else
			raise "invalid strategy"
		end
	 end
 end

In the listing above a simple TradingBot class is instantiated with a trading strategy value that it uses to select the appropriate algorithm to use at run time when the trend? method is called by a client class. A full use case may look as follows:

Example

Lets build a program to demonstrate market indicators for Apple, Inc stock

 bot12day = TradingBot.new(:twelve_day)
 bot24day = TradingBot.new(:twenty_four_day)

 data = File.read('testdata/APPL.json')

 12_day_trend = bot12day.trend?(data)
 24_day_trend = bot24day.trend?(data)

 puts "Based on two analyses the twelve day moving average suggests APPL is going #{12_day_trend} while the twenty four day moving average suggests it is going #{24_day_trend}"

By abstracting the strategy here we gain the flexibility to reuse the same TradingBot class and can also implement the algorithms elsewhere.