Class: SQA::Backtest

Inherits:
Object
  • Object
show all
Defined in:
lib/sqa/backtest.rb

Defined Under Namespace

Classes: Results

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(stock:, strategy:, start_date: nil, end_date: nil, initial_capital: 10_000.0, commission: 0.0, position_size: :all_cash) ⇒ Backtest

Initialize a backtest

Parameters:

  • stock (SQA::Stock)

    Stock to backtest

  • strategy (SQA::Strategy, Proc)

    Strategy or callable that returns :buy, :sell, or :hold

  • start_date (Date, String) (defaults to: nil)

    Start date for backtest

  • end_date (Date, String) (defaults to: nil)

    End date for backtest

  • initial_capital (Float) (defaults to: 10_000.0)

    Starting capital

  • commission (Float) (defaults to: 0.0)

    Commission per trade

  • position_size (Symbol, Float) (defaults to: :all_cash)

    :all_cash or fraction of portfolio per trade



85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/sqa/backtest.rb', line 85

def initialize(stock:, strategy:, start_date: nil, end_date: nil,
               initial_capital: 10_000.0, commission: 0.0, position_size: :all_cash)
  @stock = stock
  @strategy = strategy
  @start_date = start_date ? Date.parse(start_date.to_s) : Date.parse(stock.df["timestamp"].first)
  @end_date = end_date ? Date.parse(end_date.to_s) : Date.parse(stock.df["timestamp"].last)
  @initial_capital = initial_capital
  @commission = commission
  @position_size = position_size

  @portfolio = SQA::Portfolio.new(initial_cash: initial_capital, commission: commission)
  @equity_curve = []  # Track portfolio value over time
  @results = Results.new
end

Instance Attribute Details

#equity_curveObject (readonly)

Returns the value of attribute equity_curve.



8
9
10
# File 'lib/sqa/backtest.rb', line 8

def equity_curve
  @equity_curve
end

#portfolioObject (readonly)

Returns the value of attribute portfolio.



8
9
10
# File 'lib/sqa/backtest.rb', line 8

def portfolio
  @portfolio
end

#resultsObject (readonly)

Returns the value of attribute results.



8
9
10
# File 'lib/sqa/backtest.rb', line 8

def results
  @results
end

#stockObject (readonly)

Returns the value of attribute stock.



8
9
10
# File 'lib/sqa/backtest.rb', line 8

def stock
  @stock
end

#strategyObject (readonly)

Returns the value of attribute strategy.



8
9
10
# File 'lib/sqa/backtest.rb', line 8

def strategy
  @strategy
end

Instance Method Details

#runResults

Run the backtest

Examples:

Run backtest with RSI strategy

stock = SQA::Stock.new(ticker: 'AAPL')
backtest = SQA::Backtest.new(
  stock: stock,
  strategy: SQA::Strategy::RSI,
  initial_capital: 10_000,
  commission: 1.0
)
results = backtest.run
puts results.summary
# => Total Return: 15.5%
#    Sharpe Ratio: 1.2
#    Max Drawdown: -8.3%
#    Win Rate: 65%

Backtest with custom date range

backtest = SQA::Backtest.new(
  stock: stock,
  strategy: SQA::Strategy::MACD,
  start_date: '2023-01-01',
  end_date: '2023-12-31'
)
results = backtest.run
results.total_return  # => 0.155 (15.5%)

Access equity curve for plotting

results = backtest.run
backtest.equity_curve.each do |point|
  puts "#{point[:date]}: $#{point[:value]}"
end

Returns:



134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/sqa/backtest.rb', line 134

def run
  # Get data for the backtest period
  df = @stock.df.data

  # Filter to backtest period
  timestamps = df["timestamp"].to_a
  start_idx = timestamps.index { |t| Date.parse(t) >= @start_date } || 0
  end_idx = timestamps.rindex { |t| Date.parse(t) <= @end_date } || timestamps.length - 1

  prices = df["adj_close_price"].to_a
  ticker = @stock.ticker.upcase

  # Track current position
  current_position = nil  # :long, :short, or nil

  # Run through each day
  (start_idx..end_idx).each do |i|
    date = Date.parse(timestamps[i])
    price = prices[i]

    # Get historical prices up to this point for strategy
    historical_prices = prices[0..i]

    # Generate signal from strategy
    signal = generate_signal(historical_prices)

    # Execute trades based on signal
    case signal
    when :buy
      if current_position.nil? && can_buy?(price)
        shares = calculate_shares_to_buy(price)
        @portfolio.buy(ticker, shares: shares, price: price, date: date)
        current_position = :long
      end

    when :sell
      if current_position == :long
        pos = @portfolio.position(ticker)
        @portfolio.sell(ticker, shares: pos.shares, price: price, date: date) if pos
        current_position = nil
      end
    end

    # Record equity curve
    current_value = @portfolio.value(ticker => price)
    @equity_curve << { date: date, value: current_value, price: price }
  end

  # Close any open positions at end
  if current_position == :long
    final_price = prices[end_idx]
    final_date = Date.parse(timestamps[end_idx])
    pos = @portfolio.position(ticker)
    @portfolio.sell(ticker, shares: pos.shares, price: final_price, date: final_date) if pos
  end

  # Calculate results
  calculate_results

  @results
end