I was in Chicago this week. I was given the opportunity to attend a conference this fall, and I picked RubyConf'24. Since taking a new job in July, I am back developing code part of my time. My entire stint with NextEra Energy (over five years), I did not actively develop code that was destine for a production environment. That was one of the Faustian Bargains I had to make when accepting that job in 2019: I would not be writing software, I would be dedicating my time to managing people and projects. Prior to NextEra, I was at the Institute for Social Research and Data Innovation, and it was a Ruby shop. I was hired there in 2012 as a senior Ruby developer. And that's what I primarily did: write Ruby code. NextEra Analytics was a Kotlin/NodeJS/Python shop; there was no place for Ruby.
But, I am back, actively looking through and developing software, in Ruby, no less. So, on Wednesday of this week, I found myself sitting in the audience at Hilton Chicago, listening and watching the creator of the Ruby language, Yukihiro Matsumoto give the keynote. It was a broad based overview of Ruby, where it came from, highlights over the years and a bit of a vision on where Mats sees it going. I had forgotten how much I like writing Ruby. It is a real joy.
So, I took a little time to write some Ruby. I settled on a topic that I wrote extensively about earlier in the year: pricing stock options. Most of that work was done in Rust and I investigated different numerical methods for pricing options. I did not go that far with my Ruby, in fact, I settled on implementing Black-Scholes, which, in its basic form, is only useful for pricing European-style options. Black-Scholes does not account for the early exercise American-style options.
# frozen_string_literal: true module BlackScholes module_function OptionType = Struct.new(:name) CALL = OptionType.new(:call) PUT = OptionType.new(:put) def calculate(type:, stock_price:, strike_price:, time_to_expiry:, risk_free_rate:, volatility:) params = { type: type, stock_price: stock_price, strike_price: strike_price, time_to_expiry: time_to_expiry, risk_free_rate: risk_free_rate, volatility: volatility } validate_params(params) d1, d2 = calculate_d1_d2(stock_price, strike_price, time_to_expiry, risk_free_rate, volatility) discount_factor = Math.exp(-risk_free_rate * time_to_expiry) option_price = case type when CALL stock_price * norm_cdf(d1) - strike_price * discount_factor * norm_cdf(d2) when PUT strike_price * discount_factor * norm_cdf(-d2) - stock_price * norm_cdf(-d1) else raise ArgumentError, "Invalid option type: #{type}" end option_price.round(6) end def option_calculator_for(type) ->(params) { calculate(type: type, **params) } end def calculate_d1_d2(s, k, t, r, sigma) components = [ -> { Math.log(s / k) }, -> { r * t }, -> { (sigma**2 / 2) * t }, -> { sigma * Math.sqrt(t) } ] d1 = components.take(3).sum(&:call) / components.last.call d2 = d1 - components.last.call [d1, d2] end def norm_cdf(x) (1 + Math.erf(x / Math.sqrt(2))) / 2 end def validate_params(params) required_params = %i[type stock_price strike_price time_to_expiry risk_free_rate volatility] numeric_params = required_params - [:type] missing_params = required_params - params.keys raise ArgumentError, "Missing parameters: #{missing_params.join(', ')}" unless missing_params.empty? params.each do |key, value| case [key, value] in [:type, OptionType] next in [param, Numeric => n] if numeric_params.include?(param) raise ArgumentError, "#{param} must be positive" unless n.positive? in [param, _] if numeric_params.include?(param) raise ArgumentError, "#{param} must be a positive number" else raise ArgumentError, "Invalid parameter: #{key}" end end end end # Usage example option_params = { stock_price: 100, strike_price: 100, time_to_expiry: 1, risk_free_rate: 0.05, volatility: 0.2 } [BlackScholes::CALL, BlackScholes::PUT].each do |option_type| price = BlackScholes.calculate(type: option_type, **option_params) puts "#{option_type.name.capitalize} Option Price: #{price}" end
(base) alex:~/projects/ruby-3.3.6-bin$ ./bin/ruby bs3.rb Call Option Price: 10.450584 Put Option Price: 5.573526
Here are some of Ruby's most notable and unique features:
1. Everything is an Object
-
True: In Ruby, every entity (including primitives like numbers, booleans,
nil, and even classes) is an object. This means all these entities have methods and can respond to messages. For example, you can call theto_smethod on any object to get its string representation.
5.to_s # Returns "5" nil.to_s # Returns ""
2. Anonymous Functions (Blocks, Procs, and Lambdas)
- Supported: Ruby offers several forms of anonymous functions, each with slightly different behaviors:
-
Blocks: Not objects by themselves but can be converted to
Procobjects. They are defined withdo...endor{...}and are commonly used with methods that yield.
[1, 2, 3].each do |element| puts element end
-
Procs: Are objects and can be defined using
Proc.newor theprockeyword. They have lenient argument handling (ignoring extra args).
my_proc = proc { |a, b| puts a + b } my_proc.call(2, 3) # Outputs: 5
-
Lambdas: Also objects, defined with the
->syntax orlambdakeyword. They enforce argument counts strictly.
my_lambda = ->(a, b) { puts a + b } my_lambda.call(2, 3) # Outputs: 5
3. Open Classes
- Ruby allows you to reopen and modify classes even after they've been defined. This is useful for adding functionality to core classes or third-party libraries.
class String def greet "Hello, #{self}!" end end "World".greet # Returns "Hello, World!"
This is sometimes referred to as monkey patching.
4. Metaprogramming
- Ruby's syntax and nature make it extremely conducive to metaprogramming techniques, allowing for dynamic creation and modification of code at runtime.
5. Dynamic Typing
- Variables in Ruby do not have explicit types. The data type of a variable is determined at runtime, making the language very flexible but also potentially error-prone if not managed carefully.
6. Modules (Mixins)
- Modules are similar to classes but cannot be instantiated. They're primarily used for namespacing and as mixins to provide a way to compose a class's behavior from multiple sources.
module Greetable def greet "Hello!" end end class Person include Greetable end Person.new.greet # Returns "Hello!"