Algo Trading

Ruby for Trading Algorithms: A Beginner’s Guide

By Alex Pierrefeu13 min read
Ruby for Trading Algorithms: A Beginner’s Guide

Ruby is a readable, productive language for building trading bots, and a beginner who already knows it has no reason to switch to Python just to automate a strategy. Its trading ecosystem is thinner than Python's, so you will write more of the numerical code yourself, but that is not a bad thing when you are learning: a moving average and an RSI are a few lines each, and a backtester that handles fills and costs honestly is under a hundred. This guide sets up a Ruby project, pulls real candles from Binance's public API with the exchange's official gem, computes indicators in plain Ruby, runs a small but honest backtest with next-bar fills and commissions, and shows how the same code places a paper order through Alpaca. Every gem named here exists on RubyGems; the earlier version of this article referenced two that do not. It closes with how Quant Charts covers the same ground without code, where Quant, our coding agent, writes the strategy in Pine Script from a description.

Key points:

  • Ruby's strengths are readability and iteration speed, not raw performance; it suits research bots and broker integrations, not latency-sensitive execution.
  • Use official or maintained gems for I/O and plain Ruby for the maths; the trading-specific gems that do exist are small, and several that circulate online do not exist at all.
  • Backtest with next-bar fills and a cost per trade. Filling at the close that produced the signal is the most common way a beginner's backtest lies.
  • Keep secrets out of code. Load API keys from environment variables and start on a paper endpoint.

freeCodeCamp's full Ruby course covers the language itself. Everything below assumes you know Ruby's basics: arrays, hashes, blocks and classes.

Why Ruby, and Why Not

Ruby reads close to English, its standard library is broad, and Bundler makes dependency management painless. For a trading bot that fetches candles every few minutes, evaluates a rule, and sends an order through a REST API, that is everything you need, and the code will be shorter and clearer than most equivalents. Where Ruby is weaker is in numerical tooling: there is no pandas or NumPy of equal maturity, although rover-df and polars-df offer data frames and numo-narray offers fast arrays. Ruby is also not the tool for anything where microseconds matter; our latency standards guide explains why that is rarely a retail trader's real constraint anyway.

NeedGemNotes
Crypto market data and ordersbinance-connector-rubyBinance's official connector; public endpoints work without keys
US stock paper and live tradingalpaca-trade-apiCommunity gem for Alpaca's REST API; defaults to the paper endpoint
Interactive Brokersib-ruby, ib-apiWrappers for the TWS API; require the IB gateway running locally
HTTP and WebSocketshttparty, rest-client, faye-websocketFor any broker or data API without a dedicated gem
Data frames and arraysrover-df, polars-df, daru, numo-narrayOptional; plain arrays are enough for the examples here
Indicatorstechnical-analysis, tulirb, indicatorsSmall libraries; writing SMA and RSI yourself is a better first lesson
Secrets and storagedotenv, sqlite3Environment-based keys; local storage for candles and trades
ChartsvegaVega-Lite charts from Ruby for reviewing equity curves

Project Setup

Install a current Ruby (the 3.x series) with your operating system's package manager or a version manager, then create a project with a Gemfile. Keys live in a .env file that is listed in .gitignore, never in source.

ruby -v
mkdir ruby-trader && cd ruby-trader
bundle init

# Gemfile
source "https://rubygems.org"
gem "binance-connector-ruby"
gem "alpaca-trade-api"
gem "dotenv"

bundle install
# .env  (add this file to .gitignore)
ALPACA_API_KEY_ID=your_paper_key_id
ALPACA_API_SECRET_KEY=your_paper_secret

Fetching Candles

Binance's official Ruby connector exposes the public market data endpoints without credentials. The klines call returns arrays in which the first element is the open time in milliseconds and the fifth is the close; the snippet converts each row into a hash with symbols for keys, which is the idiomatic Ruby shape for the rest of the code.

require "binance"

client = Binance::Spot.new(base_url: "https://api.binance.com")   # no keys needed for market data

raw = client.klines(symbol: "BTCUSDT", interval: "1h", limit: 1000)

candles = raw.map do |row|
  {
    time:   Time.at(row[0] / 1000),
    open:   row[1].to_f,
    high:   row[2].to_f,
    low:    row[3].to_f,
    close:  row[4].to_f,
    volume: row[5].to_f
  }
end

puts candles.last

Prices arrive as strings and are converted with to_f. For accounting that must be exact to the cent, use BigDecimal instead of Float; for indicators and backtests Float is fine.

Indicators in Plain Ruby

LuxAlgo Relative Strength Index indicator on Quant Charts with overbought and oversold levels and a shaded band
The LuxAlgo Relative Strength Index on Quant Charts, from the Library. The Ruby RSI below reproduces the same Wilder smoothing with a length of 14 and levels at 70 and 30.

Simple Moving Average

module Indicators
  # Returns an array the same length as closes; nil until enough bars exist.
  def self.sma(closes, length)
    closes.each_index.map do |i|
      next nil if i < length - 1
      closes[(i - length + 1)..i].sum / length.to_f
    end
  end
end

Wilder's RSI

RSI compares average gains to average losses over a lookback window. Wilder's version seeds the averages with a simple mean and then smooths them recursively, which is what most charting platforms plot. Returning nil for the warm-up bars keeps the arrays aligned with the candles.

module Indicators
  def self.rsi(closes, length = 14)
    changes = closes.each_cons(2).map { |a, b| b - a }
    gains  = changes.map { |c| c > 0 ? c : 0.0 }
    losses = changes.map { |c| c < 0 ? -c : 0.0 }

    result = Array.new(closes.size, nil)
    return result if changes.size < length

    avg_gain = gains[0, length].sum / length
    avg_loss = losses[0, length].sum / length
    result[length] = rsi_value(avg_gain, avg_loss)

    (length...changes.size).each do |i|
      avg_gain = (avg_gain * (length - 1) + gains[i]) / length
      avg_loss = (avg_loss * (length - 1) + losses[i]) / length
      result[i + 1] = rsi_value(avg_gain, avg_loss)
    end
    result
  end

  def self.rsi_value(avg_gain, avg_loss)
    return 100.0 if avg_loss.zero?
    100.0 - 100.0 / (1.0 + avg_gain / avg_loss)
  end
end

An Honest Backtester

The strategy below is deliberately ordinary: go long when the 9-bar average crosses above the 20-bar average and RSI is not already above 70, and exit when the fast average crosses back below. The backtester's job is to evaluate that rule without cheating. Three rules keep it honest. A signal computed on a bar's close is filled at the next bar's open. Every fill pays a commission. And the equity curve is recorded on every bar, so drawdown is measured correctly, not only at trade exits.

class Backtester
  Trade = Struct.new(:entry_time, :entry, :exit_time, :exit, :qty) do
    def pnl = (exit - entry) * qty
  end

  def initialize(candles, capital: 10_000.0, commission_rate: 0.001)
    @candles = candles
    @capital = capital
    @commission_rate = commission_rate
  end

  def run(fast: 9, slow: 20, rsi_cap: 70)
    closes = @candles.map { |c| c[:close] }
    fast_ma = Indicators.sma(closes, fast)
    slow_ma = Indicators.sma(closes, slow)
    rsi     = Indicators.rsi(closes, 14)

    cash = @capital
    qty = 0.0
    entry_price = nil
    entry_time = nil
    trades = []
    equity = []
    pending = nil                                   # order decided on bar i, filled on bar i + 1

    @candles.each_with_index do |bar, i|
      # 1. Fill any order queued on the previous bar at this bar's open.
      if pending == :buy
        qty = (cash * (1 - @commission_rate)) / bar[:open]
        cash = 0.0
        entry_price = bar[:open]
        entry_time = bar[:time]
      elsif pending == :sell
        cash = qty * bar[:open] * (1 - @commission_rate)
        trades << Trade.new(entry_time, entry_price, bar[:time], bar[:open], qty)
        qty = 0.0
      end
      pending = nil

      # 2. Record equity on every bar.
      equity << cash + qty * bar[:close]

      # 3. Decide on this bar's close; the fill happens next bar.
      next if i < 1 || slow_ma[i].nil? || slow_ma[i - 1].nil? || rsi[i].nil?
      crossed_up   = fast_ma[i - 1] <= slow_ma[i - 1] && fast_ma[i] > slow_ma[i]
      crossed_down = fast_ma[i - 1] >= slow_ma[i - 1] && fast_ma[i] < slow_ma[i]

      if qty.zero? && crossed_up && rsi[i] < rsi_cap
        pending = :buy
      elsif qty.positive? && crossed_down
        pending = :sell
      end
    end

    { trades: trades, equity: equity }
  end
end

Notice what is absent: no order is filled on the bar that produced its signal, no position is sized with information from the future, and no helper is referenced that is not defined. A backtest you cannot run end to end is not a backtest.

Reading the Result

module Metrics
  def self.summarize(result, bars_per_year:)
    equity = result[:equity]
    trades = result[:trades]
    years = equity.size / bars_per_year.to_f
    total_return = equity.last / equity.first - 1
    cagr = (equity.last / equity.first) ** (1 / years) - 1

    peak = equity.first
    max_dd = 0.0
    equity.each do |e|
      peak = e if e > peak
      dd = e / peak - 1
      max_dd = dd if dd < max_dd
    end

    wins = trades.count { |t| t.pnl > 0 }
    {
      trades: trades.size,
      win_rate: trades.empty? ? nil : wins.to_f / trades.size,
      total_return: total_return,
      cagr: cagr,
      max_drawdown: max_dd
    }
  end
end

result = Backtester.new(candles, capital: 10_000.0, commission_rate: 0.001).run
pp Metrics.summarize(result, bars_per_year: 24 * 365)   # hourly crypto bars

Read the trade count before anything else; a rule that fired a handful of times over a thousand bars has told you nothing. Then compare the drawdown with the return, and re-run with a higher commission rate to see how quickly the edge erodes. A crossover strategy on hourly crypto data will usually show a long run of small losses in ranges punctuated by a few large trend wins, which is the character of the rule, not a bug in the code. This article deliberately quotes no results: they depend on the symbol, the interval and the dates you fetch.

From Backtest to Paper Orders

The alpaca-trade-api gem defaults to Alpaca's paper endpoint and reads the key id and secret from the ALPACA_API_KEY_ID and ALPACA_API_SECRET_KEY environment variables, which is why the .env file above uses those names. The client exposes account, positions, orders and a new_order method that takes the same fields as Alpaca's REST API.

require "dotenv/load"
require "alpaca/trade/api"

Alpaca::Trade::Api.configure do |config|
  config.endpoint   = "https://paper-api.alpaca.markets"     # paper trading only
  config.key_id     = ENV.fetch("ALPACA_API_KEY_ID")
  config.key_secret = ENV.fetch("ALPACA_API_SECRET_KEY")
end

client = Alpaca::Trade::Api::Client.new
puts client.account.inspect

def place_entry(client, symbol, qty)
  client.new_order(symbol: symbol, qty: qty, side: "buy",
                   type: "market", time_in_force: "day")
end

# Wire it to the strategy: when the live bar closes with a buy signal,
# call place_entry once, record the order id, and never resend on the same bar.

Two habits matter more than any code here. Run on paper until the bot has survived a few weeks of real market hours without an unhandled exception, and log every decision with its inputs so that when a fill surprises you, you can see what the code saw. The ib-ruby and ib-api gems offer the same pattern for Interactive Brokers, with the extra requirement that the IB gateway is running locally.

Pitfalls Specific to Beginners

PitfallSymptomFix
Same-bar fillsBacktest returns far better than paper tradingQueue the order and fill at the next bar's open, as the Backtester does
Copying code for gems that do not existLoadError on requireCheck the gem on RubyGems before adding it; two gems in the original version of this article were fictional
Keys in sourceLeaked credentials in a public repositorydotenv plus .gitignore; rotate any key that was ever committed
Float accountingPositions that do not sum to the account balanceBigDecimal for money; Float for indicators
Unhandled API errorsBot dies at 3 a.m. on a rate limitRescue the connector's error classes, back off, and alert yourself
Resending ordersDuplicate positions after a retryIdempotency: record the order id per signal bar and check before sending
Overfitting parametersBest backtest, worst live monthSplit the data, tune on one part, read the result on the other

The Same Strategy on Quant Charts

Everything above can be done without writing any Ruby. On Quant Charts, describe the rule to Quant: a 9 and 20 moving average crossover, long only, no entries when RSI is above 70, exit on the reverse cross. Quant writes the strategy in Pine Script and plots it on the active chart; open Code to read what it wrote, then click Run. The Backtest Summary reports net profit, trade count, win rate, maximum drawdown and profit factor, with commission and slippage set in the strategy properties, so the cost sensitivity test above is a settings change rather than a code change. The Making Strategies with Quant guide shows the workflow, and the Library's Relative Strength Index is the same indicator the Ruby code computes, available in a click.

Favourites and the indicator wheel in Quant Charts. Library indicators such as RSI load without code; Quant writes new ones from a description.

For a programmer the interesting bridge is PineTS, LuxAlgo's open-source TypeScript runtime for Pine Script®, which runs Pine logic natively on LuxAlgo; a Ruby developer comfortable with one scripting language will find Pine's bar-by-bar model familiar, and our guide to writing Pine Script® indicators is the natural next read. One boundary: the LuxAlgo platform does not place orders for you, which is exactly what the Alpaca snippet above does and why it belongs in your own code.

Conclusion

Ruby will not make a strategy profitable, and neither will any other language; what it offers is a pleasant, readable path from an idea to a tested rule to a paper order, with official or maintained gems for the parts that talk to exchanges and brokers and a standard library that handles the rest. The discipline is the same as everywhere else: verify that the libraries you use exist, fill on the next bar, pay a cost per trade, keep secrets out of the repository, and paper trade for longer than feels necessary. Get those right in Ruby and you will get them right in Pine Script on Quant Charts, where Quant writes the code and the Backtest Summary asks the same hard questions about cost and drawdown.

Key Takeaways

  • Verified gems only. binance-connector-ruby, alpaca-trade-api, ib-ruby and ib-api exist and are documented; quantlib-ruby and backtest do not.
  • Indicators are short. SMA and Wilder's RSI in plain Ruby are a dozen lines each and align with the chart.
  • Backtest honestly. Next-bar fills, commission on every fill, equity on every bar, and no undefined helpers.
  • Paper first, secrets in the environment. dotenv, the paper endpoint and idempotent order sending.
  • Same rule without code. Describe it to Quant on Quant Charts, read the Pine Script in Code, click Run.

FAQs

Is Ruby a good language for algorithmic trading?

For research bots, broker integrations and strategies that act on closed bars, yes: it is readable, quick to iterate and has official or maintained gems for Binance, Alpaca and Interactive Brokers. Its numerical ecosystem is thinner than Python's, so you write more of the maths yourself, and it is not suitable for latency-sensitive execution, which is rarely a retail constraint in any case.

Which Ruby gems should I use for trading?

binance-connector-ruby is Binance's official connector and serves public market data without keys. alpaca-trade-api wraps Alpaca's REST API and defaults to the paper endpoint. ib-ruby and ib-api cover Interactive Brokers. For everything else, httparty or rest-client for HTTP, faye-websocket for streams, dotenv for secrets, and rover-df or polars-df if you want data frames. Check any gem on RubyGems before requiring it.

How do I backtest a trading strategy in Ruby?

Compute indicators over the closes, decide on each bar's close, and fill the resulting order at the next bar's open with a commission applied. Record equity on every bar so drawdown is measured correctly, and summarise trade count, win rate, return and maximum drawdown. The Backtester class in this article does exactly that in plain Ruby with no undefined helpers.

How do I keep API keys safe in a Ruby trading bot?

Store them in a .env file that is listed in .gitignore, load them with the dotenv gem, and read them with ENV.fetch so the program fails loudly if one is missing. Start on a paper endpoint, never commit a key, and rotate any key that was ever committed by mistake.

Can Ruby place real orders?

Yes, through broker APIs: the alpaca-trade-api gem's new_order method takes symbol, quantity, side, type and time in force, and the Interactive Brokers gems offer the equivalent. Run on the paper endpoint first, make order sending idempotent by recording the order id per signal bar, and handle the connector's error classes so a rate limit does not stop the bot silently.

Can I build the same strategy on Quant Charts without Ruby?

Yes. Describe the rule to Quant and it writes the strategy in Pine Script, which you can inspect in Code and run with Run; the Backtest Summary reports the results with commission and slippage from the strategy properties. Library indicators such as the Relative Strength Index load in a click. The LuxAlgo platform does not place orders for you, so live execution stays in your own code, for example through our open-source Trade Relay and Broker SDK.

References

LuxAlgo Resources

External Resources

Learn to trade smarter.

Market analysis and techniques that build your edge, one email a week.

Don’t worry, no spam here. See our privacy policy for more info.

Alex Pierrefeu
Alex Pierrefeu

CPO & Co-founder at LuxAlgo. 7+ years background of developing technical trading tools, Alex is one of the very few highlighted "Pine Script Wizards" on TradingView.

Read next