SQA - Simple Qualitative Analysis
A Ruby library for technical analysis of stock market data, trading strategies, and portfolio optimization.
Core library of the SQA (Simple Qualitative Analysis) ecosystem.
Table of Contents
- Features
- Installation
- Configuration
- Usage
- Interactive Console
- Architecture
- Data Sources
- Contributing
- License
Installation
Install the gem:
gem install sqa
This will automatically install the sqa-tai gem (technical indicators) as a dependency.
Or add to your Gemfile:
gem 'sqa'
Then run:
bundle install
For command-line tools, install the separate CLI gem:
gem install sqa-cli
Configuration
Data Directory
SQA stores historical price data and metadata in a data directory. By default, it uses ~/sqa_data/:
require 'sqa'
# Initialize with default configuration
SQA.init
# Or specify a custom data directory
SQA::Config.new(data_dir: '~/Documents/my_stock_data')
Create your data directory:
mkdir ~/sqa_data
Alpha Vantage API Key
SQA uses Alpha Vantage for stock data. You'll need a free API key from https://www.alphavantage.co/
Set the environment variable:
export AV_API_KEY="your_api_key_here"
# or
export ALPHAVANTAGE_API_KEY="your_api_key_here"
The free tier allows:
- 5 API calls per minute
- 100 API calls per day
Usage
Working with Stocks
require 'sqa'
# Initialize SQA
SQA.init
# Create or load a stock (automatically fetches/updates data from Alpha Vantage)
aapl = SQA::Stock.new(ticker: 'AAPL', source: :alpha_vantage)
#=> aapl with 1207 data points from 2019-01-02 to 2023-10-17
# Access the DataFrame
aapl.df
#=> SQA::DataFrame (wraps Polars::DataFrame)
# Get column names
aapl.df.columns
#=> ["timestamp", "open_price", "high_price", "low_price", "close_price", "adj_close_price", "volume"]
# Access price data
aapl.df["adj_close_price"]
#=> Polars::Series with all adjusted closing prices
# Get recent prices
prices = aapl.df["adj_close_price"].to_a
prices.last(5)
#=> [179.8, 180.71, 178.85, 178.72, 177.15]
# Access company metadata
aapl.name
#=> "Apple Inc."
aapl.exchange
#=> "NASDAQ"
aapl.overview
#=> Hash with detailed company information
DataFrame Operations
SQA::DataFrame is a wrapper around the high-performance Polars DataFrame:
require 'sqa'
# Create from hash
data = {
timestamp: ['2023-01-01', '2023-01-02', '2023-01-03'],
price: [100.0, 101.5, 99.8]
}
df = SQA::DataFrame.new(data)
# Load from CSV
df = SQA::DataFrame.load(source: 'aapl.csv')
# Basic operations
df.size # Number of rows
df.ncols # Number of columns
df.to_h # Convert to hash
df.to_csv('output.csv') # Save to CSV
# Access underlying Polars DataFrame
df.data # Polars::DataFrame
Technical Indicators
All technical indicators are provided by the separate sqa-tai gem, which wraps the industry-standard TA-Lib library. The sqa-tai gem must be installed separately but is automatically included as a dependency of sqa.
require 'sqa'
prices = [100, 102, 105, 103, 107, 110, 108, 112, 115, 113]
# Simple Moving Average
sma = SQAI.sma(prices, period: 5)
#=> [104.0, 105.4, 106.6, 108.0, 110.4, 111.6]
# Relative Strength Index
rsi = SQAI.rsi(prices, period: 14)
#=> [70.5, 68.2, ...]
# Exponential Moving Average
ema = SQAI.ema(prices, period: 5)
#=> [...]
# Bollinger Bands (returns upper, middle, lower)
upper, middle, lower = SQAI.bbands(prices, period: 5)
# MACD
macd, signal, histogram = SQAI.macd(prices, fast_period: 12, slow_period: 26, signal_period: 9)
# Many more indicators available!
# See: https://github.com/MadBomber/sqa-tai
Available Indicators: SMA, EMA, WMA, RSI, MACD, Bollinger Bands, Stochastic, ADX, ATR, CCI, Williams %R, ROC, Momentum, and 140+ more via TA-Lib.
Trading Strategies
Build and test trading strategies:
require 'sqa'
require 'ostruct'
# Load strategies
strategies = SQA::Strategy.new
strategies.auto_load # Loads all built-in strategies
# Add specific strategies
strategies.add SQA::Strategy::RSI
strategies.add SQA::Strategy::SMA
strategies.add SQA::Strategy::EMA
# Prepare data for strategy execution
prices = aapl.df["adj_close_price"].to_a
rsi_value = SQAI.rsi(prices, period: 14).last
vector = OpenStruct.new
vector.rsi = rsi_value
vector.prices = prices
# Execute strategies
results = strategies.execute(vector)
#=> [:hold, :buy, :hold] # One result per strategy
# Built-in strategies:
# - SQA::Strategy::RSI - Based on Relative Strength Index
# - SQA::Strategy::SMA - Simple Moving Average crossover
# - SQA::Strategy::EMA - Exponential Moving Average crossover
# - SQA::Strategy::MACD - MACD crossover strategy
# - SQA::Strategy::BollingerBands - Bollinger Bands bounce strategy
# - SQA::Strategy::Stochastic - Stochastic oscillator strategy
# - SQA::Strategy::VolumeBreakout - Volume-based breakout strategy
# - SQA::Strategy::MR - Mean Reversion
# - SQA::Strategy::MP - Market Profile
# - SQA::Strategy::KBS - Knowledge-based RETE strategy (advanced)
# - SQA::Strategy::Random - Random signal generator (for testing)
# - SQA::Strategy::Consensus - Combines multiple strategies
Statistics
Comprehensive statistical analysis on price data (via lite-statistics gem):
require 'sqa'
prices = [179.8, 180.71, 178.85, 178.72, 177.15]
stats = prices.summary
#=> {
# max: 180.71,
# min: 177.15,
# mean: 179.046,
# median: 178.85,
# mode: nil,
# range: 3.56,
# sample_standard_deviation: 1.19,
# sample_variance: 1.42,
# ...
# }
Portfolio Management
Track positions, trades, and P&L:
require 'sqa'
# Create portfolio with initial cash and commission
portfolio = SQA::Portfolio.new(initial_cash: 10_000.0, commission: 1.0)
# Buy stock
portfolio.buy('AAPL', shares: 10, price: 150.0, date: Date.today)
# Sell stock
portfolio.sell('AAPL', shares: 5, price: 160.0, date: Date.today)
# Check portfolio value
current_prices = { 'AAPL' => 165.0 }
portfolio.value(current_prices)
#=> Total portfolio value
# Calculate profit/loss
portfolio.profit_loss(current_prices)
#=> P&L amount
# View summary
portfolio.summary
#=> { initial_cash: 10000.0, cash: 8248.0, positions: 1, trades: 2, ... }
# Save trades to CSV
portfolio.save_to_csv('my_trades.csv')
# Load from CSV
portfolio.load_from_csv('my_trades.csv')
Backtesting
Simulate trading strategies against historical data:
require 'sqa'
SQA.init
stock = SQA::Stock.new(ticker: 'AAPL')
# Create backtest
backtest = SQA::Backtest.new(
stock: stock,
strategy: SQA::Strategy::RSI,
initial_capital: 10_000.0,
commission: 1.0
)
# Run backtest
results = backtest.run
# View comprehensive metrics
puts "Total Return: #{results.total_return.round(2)}%"
puts "Annualized Return: #{results.annualized_return.round(2)}%"
puts "Sharpe Ratio: #{results.sharpe_ratio.round(2)}"
puts "Max Drawdown: #{results.max_drawdown.round(2)}%"
puts "Total Trades: #{results.total_trades}"
puts "Win Rate: #{results.win_rate.round(2)}%"
puts "Profit Factor: #{results.profit_factor.round(2)}"
# Access equity curve for charting
results.equity_curve #=> Array of portfolio values over time
Strategy Generator
Reverse-engineer profitable trades to discover winning patterns:
require 'sqa'
SQA.init
stock = SQA::Stock.new(ticker: 'AAPL')
# Create strategy generator
generator = SQA::StrategyGenerator.new(
stock: stock,
min_gain_percent: 10.0, # Find trades with ≥10% gain
holding_period: (5..20) # Within 5-20 days
)
# Discover patterns
patterns = generator.discover_patterns(min_pattern_frequency: 3)
# Print discovered patterns
generator.print_patterns(max_patterns: 10)
#=> Pattern #1:
#=> Frequency: 15 occurrences
#=> Average Gain: 12.5%
#=> Average Holding: 8.3 days
#=> Conditions:
#=> - rsi: oversold
#=> - macd_crossover: bullish
#=> - volume: high
# Generate strategy from top pattern
strategy = generator.generate_strategy(pattern_index: 0, strategy_type: :class)
# Backtest the discovered strategy
backtest = SQA::Backtest.new(stock: stock, strategy: strategy)
results = backtest.run
# Export patterns to CSV
generator.export_patterns('/tmp/patterns.csv')
Genetic Programming
Evolve optimal strategy parameters through natural selection:
require 'sqa'
SQA.init
stock = SQA::Stock.new(ticker: 'AAPL')
# Create genetic program
gp = SQA::GeneticProgram.new(
stock: stock,
population_size: 50,
generations: 100,
mutation_rate: 0.15
)
# Define parameter space to explore
gp.define_genes(
rsi_period: (7..30).to_a,
buy_threshold: (20..40).to_a,
sell_threshold: (60..80).to_a
)
# Define fitness function (backtest performance)
gp.fitness do |genes|
# Create strategy with these genes
strategy = create_rsi_strategy(genes)
# Backtest and return total return as fitness
backtest = SQA::Backtest.new(stock: stock, strategy: strategy)
backtest.run.total_return
end
# Evolve!
best = gp.evolve
puts "Best Parameters:"
puts " RSI Period: #{best.genes[:rsi_period]}"
puts " Buy Threshold: #{best.genes[:buy_threshold]}"
puts " Sell Threshold: #{best.genes[:sell_threshold]}"
puts " Fitness: #{best.fitness.round(2)}%"
# View evolution history
gp.history.each do |gen|
puts "Gen #{gen[:generation]}: Best=#{gen[:best_fitness].round(2)}%"
end
Knowledge-Based Strategy (KBS)
Build sophisticated rule-based systems with RETE forward chaining:
require 'sqa'
# Create KBS strategy
strategy = SQA::Strategy::KBS.new(load_defaults: false)
# Define custom trading rules
strategy.add_rule :golden_opportunity do
desc "Perfect storm: Multiple bullish indicators align"
# Multiple conditions
on :rsi, { level: :oversold }
on :macd, { crossover: :bullish }
on :stochastic, { zone: :oversold, crossover: :bullish }
on :trend, { short_term: :up, strength: :strong }
on :volume, { level: :high }
# Negation: Don't buy if overbought elsewhere
without :rsi, { level: :overbought }
# Action
then do
assert(:signal, { action: :buy, confidence: :high })
end
end
# Execute strategy
signal = strategy.execute(vector)
#=> :buy or :sell or :hold
# Use with backtesting
backtest = SQA::Backtest.new(
stock: stock,
strategy: SQA::Strategy::KBS,
initial_capital: 10_000.0
)
results = backtest.run
Real-Time Streaming
Process live stock prices and generate on-the-fly trading signals:
require 'sqa'
# Create stream processor
stream = SQA::Stream.new(
ticker: 'AAPL',
window_size: 100,
strategies: [
SQA::Strategy::RSI,
SQA::Strategy::MACD,
SQA::Strategy::BollingerBands
]
)
# Register signal callback
stream.on_signal do |signal, data|
puts "🔔 SIGNAL: #{signal.upcase}"
puts " Price: $#{data[:price]}"
puts " Consensus: #{data[:strategies_vote]}"
# Execute trade, send alert, etc.
execute_trade(signal, data) if signal != :hold
end
# Register update callback (optional)
stream.on_update do |data|
puts "📊 Price update: $#{data[:price]}"
end
# Feed live data (from WebSocket, API, etc.)
stream.update(
price: 150.25,
volume: 1_000_000,
timestamp: Time.now
)
# Access real-time indicators
rsi = stream.indicator(:rsi, period: 14)
sma = stream.indicator(:sma, period: 20)
# View stream statistics
stream.stats
#=> { ticker: 'AAPL', updates: 125, current_price: 150.25, ... }
FPL Analysis (Future Period Loss/Profit)
Analyze potential future price movements with risk metrics:
require 'sqa'
stock = SQA::Stock.new(ticker: 'AAPL')
# Calculate FPL for 14-day future period
fpl_data = stock.df.fpl(fpop: 14)
# Returns: [[min_delta, max_delta], ...] for each price point
# Full analysis with risk metrics
analysis = stock.df.fpl_analysis(fpop: 14)
analysis.first
#=> {
# min_delta: -5.2, # Worst loss percentage
# max_delta: 8.7, # Best gain percentage
# risk: 13.9, # Volatility (max - min)
# direction: :UNCERTAIN,# :UP, :DOWN, :UNCERTAIN, :FLAT
# magnitude: 1.75, # Average expected movement
# interpretation: "UNCERTAIN: 1.75% (±6.95% risk)"
#}
# Filter high-quality opportunities
good_indices = SQA::FPOP.filter_by_quality(
analysis,
min_magnitude: 5.0, # Minimum average movement
max_risk: 10.0, # Maximum acceptable volatility
directions: [:UP] # Only bullish patterns
)
# Use with Strategy Generator for risk-aware pattern discovery
generator = SQA::StrategyGenerator.new(
stock: stock,
min_gain_percent: 10.0,
fpop: 10,
max_fpl_risk: 20.0, # NEW: Filter high-volatility points
required_fpl_directions: [:UP] # NEW: Only bullish patterns
)
Key Benefits:
- Captures both upside AND downside potential
- Measures volatility/risk during future period
- Classifies directional bias (UP/DOWN/UNCERTAIN/FLAT)
- Enables risk-adjusted opportunity filtering
Market Regime Detection
Identify and adapt to changing market conditions:
# Detect current market regime
regime = SQA::MarketRegime.detect(stock)
#=> {
# type: :bull, # :bull, :bear, :sideways
# volatility: :low, # :low, :medium, :high
# strength: :strong, # :weak, :moderate, :strong
# lookback_days: 60,
# detected_at: 2024-11-08
#}
# Analyze regime history
regimes = SQA::MarketRegime.detect_history(stock)
#=> [
# { type: :bull, start_index: 0, end_index: 120, duration: 120 },
# { type: :bear, start_index: 121, end_index: 200, duration: 79 },
# ...
#]
# Split data by regime
regime_data = SQA::MarketRegime.split_by_regime(stock)
#=> {
# bull: [{ prices: [...], start_index: 0, end_index: 120 }, ...],
# bear: [...],
# sideways: [...]
#}
# Use regime-specific strategies
current_regime = SQA::MarketRegime.detect(stock)
strategy = case current_regime[:type]
when :bull
SQA::Strategy::MomentumBreakout
when :bear
SQA::Strategy::MeanReversion
when :sideways
SQA::Strategy::BollingerBands
end
Key Benefits:
- Different strategies for different market conditions
- Avoid using bull market strategies in bear markets
- Detect regime changes before they impact performance
Seasonal Analysis
Discover calendar-dependent patterns:
# Analyze seasonal performance
seasonal = SQA::SeasonalAnalyzer.analyze(stock)
#=> {
# best_months: [10, 11, 12], # October, November, December
# worst_months: [5, 6, 7], # May, June, July
# best_quarters: [4, 1], # Q4 and Q1
# worst_quarters: [2, 3],
# has_seasonal_pattern: true,
# monthly_returns: { ... },
# quarterly_returns: { ... }
#}
# Filter data for Q4 only (holiday shopping season)
q4_data = SQA::SeasonalAnalyzer.filter_by_quarters(stock, [4])
#=> { indices: [...], dates: [...], prices: [...] }
# Filter for specific months
dec_data = SQA::SeasonalAnalyzer.filter_by_months(stock, [12])
# Get context for specific date
context = SQA::SeasonalAnalyzer.context_for_date(Date.new(2024, 12, 15))
#=> {
# month: 12,
# quarter: 4,
# month_name: "December",
# quarter_name: "Q4",
# is_year_end: true,
# is_holiday_season: true,
# is_earnings_season: false
#}
# Discover patterns only for best months
generator = SQA::StrategyGenerator.new(stock: stock)
patterns = generator.discover_context_aware_patterns(
analyze_seasonal: true
)
patterns.each do |pattern|
puts pattern.context.valid_months # [10, 11, 12] for Q4 patterns
end
Real-World Examples:
- Retail stocks: Q4 holiday shopping boost
- Tax prep stocks: Q1/Q4 seasonal surge
- Energy stocks: Summer driving season
- Agriculture stocks: Planting/harvest cycles
Sector Analysis with KBS
Analyze patterns across related stocks using KBS blackboards:
# Create sector analyzer (separate blackboard per sector)
analyzer = SQA::SectorAnalyzer.new
# Add stocks to technology sector
analyzer.add_stock('AAPL', sector: :technology)
analyzer.add_stock('MSFT', sector: :technology)
analyzer.add_stock('GOOGL', sector: :technology)
# Detect sector-wide regime
tech_stocks = [
SQA::Stock.new(ticker: 'AAPL'),
SQA::Stock.new(ticker: 'MSFT'),
SQA::Stock.new(ticker: 'GOOGL')
]
sector_regime = analyzer.detect_sector_regime(:technology, tech_stocks)
#=> {
# sector: :technology,
# consensus_regime: :bull, # Majority vote
# sector_strength: 85.0, # % of stocks bullish
# stock_regimes: [...]
#}
# Discover patterns across entire sector
sector_patterns = analyzer.discover_sector_patterns(
:technology,
tech_stocks,
min_gain_percent: 10.0,
fpop: 10
)
# Patterns that work for MULTIPLE stocks in sector
sector_patterns.each do |sp|
puts "Pattern found in: #{sp[:stocks].join(', ')}"
puts "Avg gain: #{sp[:avg_gain].round(2)}%"
puts "Conditions: #{sp[:conditions]}"
end
# Query sector blackboard
facts = analyzer.query_sector(:technology, :sector_pattern)
# Print sector summary
analyzer.print_sector_summary(:technology)
Available Sectors:
:technology, :finance, :healthcare, :energy, :consumer, :industrial, :materials, :utilities, :real_estate, :communications
Key Benefits:
- Leverage "stocks move together" in same sector
- Find patterns valid across multiple related stocks
- Persistent KBS blackboard tracks sector knowledge
- Cross-stock pattern validation
Walk-Forward Validation
Prevent overfitting with time-series cross-validation:
generator = SQA::StrategyGenerator.new(
stock: stock,
min_gain_percent: 10.0,
fpop: 10
)
# Run walk-forward validation
results = generator.walk_forward_validate(
train_size: 250, # 1 year training window
test_size: 60, # 3 months testing window
step_size: 30 # Step forward 1 month each iteration
)
# Only patterns that work out-of-sample
validated_patterns = results[:validated_patterns]
# Detailed results for each iteration
results[:validation_results].each do |r|
puts "Train: #{r[:train_period]}"
puts "Test: #{r[:test_period]}"
puts "Return: #{r[:test_return]}%"
puts "Sharpe: #{r[:test_sharpe]}"
end
# Statistics
all_returns = results[:validation_results].map { |r| r[:test_return] }
avg_return = all_returns.sum / all_returns.size
puts "Average out-of-sample return: #{avg_return.round(2)}%"
How It Works:
- Split data into train/test windows
- Discover patterns on training data
- Test patterns on future (unseen) data
- Roll forward and repeat
- Only keep patterns that work out-of-sample
Prevents:
- Curve-fitting to historical noise
- Patterns that only worked in the past
- Overconfidence in backtest results
Pattern Context System
Context-aware patterns that know when/where they're valid:
# Discover patterns with full context
generator = SQA::StrategyGenerator.new(stock: stock, min_gain_percent: 10.0)
patterns = generator.discover_context_aware_patterns(
analyze_regime: true,
analyze_seasonal: true,
sector: :technology
)
# Each pattern has rich context metadata
pattern = patterns.first
pattern.context.market_regime #=> :bull
pattern.context.volatility_regime #=> :low
pattern.context.valid_months #=> [10, 11, 12]
pattern.context.valid_quarters #=> [4, 1]
pattern.context.sector #=> :technology
pattern.context.discovered_period #=> "2020-01-01 to 2022-12-31"
# Runtime validation - check if pattern applies NOW
valid = pattern.context.valid_for?(
date: Date.today,
regime: :bull,
sector: :technology
)
if valid
# Execute strategy - context matches current conditions
signal = strategy.trade(vector)
else
# Skip - pattern not valid in current context
puts "Pattern not valid for current conditions"
end
# Pattern summary includes context
puts pattern.to_s
#=> "Pattern(conditions=3, freq=12, gain=11.5%, success=75.0%) [bull months:10,11,12 technology]"
Context Metadata:
- Market regime: Bull/bear/sideways classification
- Seasonality: Valid months/quarters
- Sector: Industry classification
- Volatility regime: Low/medium/high
- Discovery period: When pattern was found
- Stability score: Consistency over time
Key Benefits:
- Patterns aren't universal - they're context-specific
- Avoid using patterns in wrong market conditions
- Seasonal awareness (Q4 retail patterns, etc.)
- Sector-specific strategies
- Runtime validation before execution
Interactive Console
Launch an interactive Ruby console with SQA loaded:
sqa-console
This opens IRB with the SQA library pre-loaded, allowing you to experiment interactively:
# Already loaded: require 'sqa'
SQA.init
stock = SQA::Stock.new(ticker: 'MSFT')
prices = stock.df["close_price"].to_a
sma = SQAI.sma(prices, period: 20)
Examples
The examples/ directory contains comprehensive demonstrations of advanced features:
genetic_programming_example.rb- Evolve RSI parameters through natural selectionkbs_strategy_example.rb- Build rule-based trading systems with RETErealtime_stream_example.rb- Process live price streams with callbacksstrategy_generator_example.rb- Mine profitable patterns from history
Run examples:
ruby examples/genetic_programming_example.rb
ruby examples/strategy_generator_example.rb
See examples/README.md for detailed documentation and integration patterns.
Architecture
Core Components:
SQA::DataFrame- High-performance Polars-based data container for time seriesSQA::Stock- Represents a stock with price history and metadataSQA::Ticker- Stock symbol validation and lookupSQA::Strategy- Trading strategy execution frameworkSQA::Config- Configuration managementSQAI- Alias forSQA::TAI(technical indicators from sqa-tai gem)
Advanced Components:
SQA::Portfolio- Position and trade tracking with P&L calculationsSQA::Backtest- Strategy simulation with comprehensive metricsSQA::StrategyGenerator- Pattern mining from profitable historical tradesSQA::GeneticProgram- Evolutionary algorithm for parameter optimizationSQA::Strategy::KBS- RETE-based forward-chaining inference engineSQA::Stream- Real-time price stream processor with event callbacks
Data Flow:
- Create
SQA::Stockwith ticker symbol - Stock fetches data from Alpha Vantage or Yahoo Finance
- Data stored in Polars-based
SQA::DataFrame - Apply technical indicators via
SQAI/SQA::TAI - Execute trading strategies to generate signals
- Analyze results with statistical functions
Design Patterns:
- Plugin architecture for indicators and strategies
- Data source abstraction (Alpha Vantage, Yahoo Finance)
- Delegation to Polars for DataFrame operations
- Configuration hierarchy: defaults < environment variables < config file
Data Sources
Alpha Vantage
Recommended data source with a well-documented API.
- URL: https://www.alphavantage.co/
- API Key: Required (free tier available)
- Environment Variable:
AV_API_KEYorALPHAVANTAGE_API_KEY - Rate Limits: 5 calls/minute, 100 calls/day (free tier)
stock = SQA::Stock.new(ticker: 'GOOGL', source: :alpha_vantage)
Yahoo Finance
No API available - uses web scraping for historical data.
- URL: https://finance.yahoo.com/
- Manual Download: Download CSV files and place in
SQA.data_dir - Filename Format:
ticker.csv(lowercase), e.g.,aapl.csv
To manually download:
- Visit https://finance.yahoo.com/quote/AAPL/history?p=AAPL
- Download historical data as CSV
- Move to your data directory as
aapl.csv
stock = SQA::Stock.new(ticker: 'AAPL', source: :yahoo_finance)
Contributing
Contributions are welcome! Got an idea for a new indicator or strategy? Want to improve the math or signals?
- Bug reports and pull requests: https://github.com/MadBomber/sqa
- Technical indicators: Contribute to sqa-tai
License
The gem is available as open source under the terms of the MIT License.
Remember: This is an educational tool. Historical performance is not an indicator of future results. Never use this for real trading decisions.