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.
Video: Ruby Full Course
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.
| Need | Gem | Notes |
|---|---|---|
| Crypto market data and orders | binance-connector-ruby | Binance's official connector; public endpoints work without keys |
| US stock paper and live trading | alpaca-trade-api | Community gem for Alpaca's REST API; defaults to the paper endpoint |
| Interactive Brokers | ib-ruby, ib-api | Wrappers for the TWS API; require the IB gateway running locally |
| HTTP and WebSockets | httparty, rest-client, faye-websocket | For any broker or data API without a dedicated gem |
| Data frames and arrays | rover-df, polars-df, daru, numo-narray | Optional; plain arrays are enough for the examples here |
| Indicators | technical-analysis, tulirb, indicators | Small libraries; writing SMA and RSI yourself is a better first lesson |
| Secrets and storage | dotenv, sqlite3 | Environment-based keys; local storage for candles and trades |
| Charts | vega | Vega-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

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
| Pitfall | Symptom | Fix |
|---|---|---|
| Same-bar fills | Backtest returns far better than paper trading | Queue the order and fill at the next bar's open, as the Backtester does |
| Copying code for gems that do not exist | LoadError on require | Check the gem on RubyGems before adding it; two gems in the original version of this article were fictional |
| Keys in source | Leaked credentials in a public repository | dotenv plus .gitignore; rotate any key that was ever committed |
| Float accounting | Positions that do not sum to the account balance | BigDecimal for money; Float for indicators |
| Unhandled API errors | Bot dies at 3 a.m. on a rate limit | Rescue the connector's error classes, back off, and alert yourself |
| Resending orders | Duplicate positions after a retry | Idempotency: record the order id per signal bar and check before sending |
| Overfitting parameters | Best backtest, worst live month | Split 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.
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
- Quant Charts
- LuxAlgo Quant
- Making Strategies with Quant
- PineTS Documentation
- Relative Strength Index Indicator
- Execution Cost Modeling Concept
- In-Sample and Out-of-Sample Split Concept
- How to Build a Backtesting Engine in Python
- How to Write Pine Script for Trading Indicators
- Choosing an Algorithmic Trading Platform or API
- Backtesting Traps: Common Errors to Avoid
- Latency Standards in Trading Systems
External Resources
Read next