Getting Started with stocksimpy
Welcome to stocksimpy! This guide will walk you through the core concepts and help you run your first backtest in minutes.
Table of Contents
Installation
Prerequisites
Python 3.7+
pip
Install stocksimpy
pip install stocksimpy
Verify installation
from stocksimpy import StockData, Backtester, Strategy, Performance, Visualize
print("stocksimpy is ready!")
Quick Start
Here’s a 30-second backtest:
from stocksimpy import *
# 1. Load data
data = StockData.from_yfinance(["AAPL"], days_before=365)
# 2. Create backtester with a built-in strategy
bt = Backtester("AAPL", data, Strategy.rsi_momentum_fixed())
# 3. Run backtest
bt.run_backtest_fixed()
# 4. Analyze results
perf = Performance(bt)
print(perf.generate_risk_report())
# 5. Visualize
viz = Visualize(bt)
viz.visualize_backtest().show()
Core Concepts
StockData
What it does: Loads, validates, and manages OHLCV (Open, High, Low, Close, Volume) and indicators stock price data.
Key features:
Supports multiple data sources: yfinance, CSV, Excel, SQL, JSON, or pandas DataFrames
Validates data integrity (no missing values, duplicate dates, or negative volumes)
Handles multi-ticker data automatically
Can implement indicators with built-in methods
Example:
from stocksimpy import StockData
# Directly from yfinance
data = StockData.from_yfinance(["AAPL", "GOOGL"], days_before=365)
# From CSV file
data = StockData.from_csv("stock_prices.csv")
# From pandas DataFrame
import pandas as pd
df = pd.read_csv("data.csv", index_col="Date", parse_dates=True)
data = StockData(df)
Indicators
What it does: Contains calculations for built-in indicators, such as SMA, RSI or MACD line, that can be directly implemented into the StockData throught .add_indicator
Key features:
Contains a variety of indicator calculations
SMA, WMA, EMA, wilders_smoothing, DEMA, TEMA, HMA, RSI, macd line, wilders_macd, tema_macd, hma_macd
Each method returns a dictionary of the calculated values
Examples:
from stocksimpy import *
# Load Data
data = StockData.from_yfinance(["AAPL", "GOOGL"], days_before=365)
# Calculates SMA with a window of 20, and adds it as "sma_20" into the data
data.add_indicator(
calculate_sma, "close" 20
)
# The functions can also be used stand alone
sma = calculate_sma(data["close", "AAPL"], 20)
Backtester
What it does: Executes a trading strategy on historical data and tracks the portfolio state (cash, holdings, trades, value over time).
Key features:
Two execution modes: fixed-size (same shares per trade) or dynamic-size (variable shares)
Simulates realistic trading: applies transaction fees, tracks all trades
Multi-ticker support: automatically filters to a single symbol
Portfolio tracking: records every trade and portfolio value at each timestep
Example:
from stocksimpy import Backtester, StockData
data = StockData.from_yfinance(["AAPL"], days_before=365)
bt = Backtester(
symbol="AAPL",
data=data,
strategy=my_strategy,
initial_cap=100000, # Starting cash
transaction_fee=10, # Fee per trade
trade_amount=5000 # Used in fixed-size mode
)
# Run fixed-size backtest
bt.run_backtest_fixed()
# Or run dynamic-size backtest
bt.run_backtest_dynamic()
Strategy
What it does: Encapsulates trading logic. Strategies receive historical data and return a signal (‘buy’, ‘sell’, or ‘hold’). Remember you can use Strategy class for built-in strategies.
Two types:
Fixed-size strategy
Signature:
strategy(data: DataFrame) -> strAlways trades the same number of shares (determined by
trade_amount)Simple and easy to reason about
Dynamic-size strategy
Signature:
strategy(data: DataFrame, holdings: float) -> (str, int)Can vary the number of shares per trade
More flexibility for advanced trading logic
Example:
def my_fixed_strategy(data):
"""Buy when price is below its 20-day average."""
close = data['Close']
sma_20 = close.rolling(20).mean()
if close.iloc[-1] < sma_20.iloc[-1]:
return 'buy'
return 'hold'
def my_dynamic_strategy(data, holdings):
"""Buy more when oversold (RSI < 30)."""
# Compute RSI
delta = data['Close'].diff()
gains = delta.where(delta > 0, 0)
losses = -delta.where(delta < 0, 0)
avg_gain = gains.rolling(14).mean()
avg_loss = losses.rolling(14).mean()
rs = avg_gain / avg_loss
rsi = 100 - (100 / (1 + rs))
if rsi.iloc[-1] < 30:
shares = int(10000 / data['Close'].iloc[-1]) # Allocate $10k
return 'buy', shares
return 'hold', 0
Portfolio
What it does: Tracks the state of cash and holdings during a backtest.
Key attributes:
cash: Current available cashholdings: Current shares ownedvalue_history: Time series of total portfolio value (cash + holdings value)trade_log: DataFrame with all executed trades
Example:
bt = Backtester("AAPL", data, my_strategy)
bt.run_backtest_fixed()
# Access portfolio results
print(f"Final cash: ${bt.portfolio.cash:.2f}")
print(f"Final holdings: {bt.portfolio.holdings} shares")
print(f"Total value over time:\n{bt.portfolio.value_history}")
print(f"All executed trades:\n{bt.portfolio.trade_log}")
Performance
What it does: Computes performance metrics from a completed backtest.
Available metrics:
Daily returns: Percentage change in portfolio value each day
Total return: Overall gain/loss percentage
Annualized return: Scaled to annual percentage
Maximum drawdown: Largest peak-to-trough decline
Sharpe ratio: Risk-adjusted return (assumes 2% risk-free rate)
Volatility: Standard deviation of daily returns
Example:
from stocksimpy import Performance
bt = Backtester("AAPL", data, Strategy.rsi_momentum_fixed())
bt.run_backtest_fixed()
perf = Performance(bt)
print(perf.generate_risk_report()) # Pretty-printed metrics
print(f"Sharpe Ratio: {perf.calc_sharpe_ratio():.2f}")
print(f"Max Drawdown: {perf.calc_max_drawdown():.2%}")
Visualize
What it does: Plots backtest results with price, portfolio value, and trade markers.
Example:
from stocksimpy import Visualize
viz = Visualize(bt)
plt = viz.visualize_backtest()
# Display or save
plt.show()
# Or: plt.savefig("backtest_results.png")
Loading Data
From Yahoo Finance (yfinance)
Fastest way to get started:
from stocksimpy import StockData
# Load last 365 days
data = StockData.from_yfinance(["AAPL"], days_before=365)
# Load multiple symbols
data = StockData.from_yfinance(["AAPL", "GOOGL", "MSFT"], days_before=730)
# Load between specific dates
import datetime
start = datetime.date(2022, 1, 1)
end = datetime.date(2023, 12, 31)
data = StockData.from_yfinance(["AAPL"], start_date=start, end_date=end)
From CSV File
from stocksimpy import StockData
data = StockData.from_csv(
"historical_prices.csv",
index_col="Date", # Column name for dates
parse_dates=True # Parse as datetime
)
Expected CSV format:
Date,Open,High,Low,Close,Volume
2023-01-01,100.0,105.0,99.0,102.0,1000000
2023-01-02,102.0,106.0,101.0,104.0,1200000
...
From Pandas DataFrame
import pandas as pd
from stocksimpy import StockData
df = pd.read_csv("prices.csv", index_col="Date", parse_dates=True)
data = StockData(df)
From SQL Database
from stocksimpy import StockData
import sqlite3
conn = sqlite3.connect("stock_data.db")
df = pd.read_sql("SELECT * FROM prices", conn, index_col="Date", parse_dates=True)
data = StockData(df)
Adding Indicators
You can add technical indicators to your data for use in strategies:
from stocksimpy import StockData, Indicators
data = StockData.from_yfinance(["AAPL"], days_before=365)
# Add 20-day SMA
data.add_indicator(Indicators.sma, base_col = "Close", window=20)
# Add 14-day RSI
data.add_indicator(Indicators.rsi, base_col = "Close", window=14)
Indicators module offers many built-in indicators like SMA, EMA, RSI, MACD, etc. You can also add custom indicators by passing a function that takes the data (as a pandas Series) and returns a dictionary of Series, with the keys being the indicator names and the values being the Series.
Here is an example of adding a custom indicator:
def my_custom_indicator(data: pd.Series) -> dict:
# Example: 10-day volatility (standard deviation of returns)
returns = data.pct_change()
volatility = returns.rolling(10).std()
return {'volatility_10d': volatility}
data.add_indicator(my_custom_indicator, base_col="Close")
Running a Backtest
Fixed-Size Backtest
Use when you want to trade the same dollar amount each time:
from stocksimpy import Backtester, StockData, Strategy
data = StockData.from_yfinance(["AAPL"], days_before=365)
bt = Backtester(
symbol="AAPL",
data=data,
strategy=Strategy.sma_ema_crossover_fixed(fast=12, slow=26),
initial_cap=100000, # Start with $100k
trade_amount=5000, # Trade $5k per signal
transaction_fee=10 # $10 per trade
)
# Run the backtest
bt.run_backtest_fixed()
# Check results
report = bt.generate_report()
print(report)
Dynamic-Size Backtest
Use when you want the strategy to decide position size:
from stocksimpy import Backtester, StockData, Strategy
data = StockData.from_yfinance(["AAPL"], days_before=365)
bt = Backtester(
symbol="AAPL",
data=data,
strategy=Strategy.price_action_dynamic(),
initial_cap=100000,
transaction_fee=10
)
# Run the backtest (strategy determines shares per trade)
bt.run_backtest_dynamic()
# Check results
report = bt.generate_report()
print(report)
Analyzing Results
View Performance Metrics
from stocksimpy import Performance
perf = Performance(bt)
# Print all metrics
print(perf.generate_risk_report())
# Access individual metrics
print(f"Total Return: {perf.calc_total_return():.2%}")
print(f"Annualized Return: {perf.get_annualized_return():.2%}")
print(f"Max Drawdown: {perf.calc_max_drawdown():.2%}")
print(f"Sharpe Ratio: {perf.calc_sharpe_ratio():.2f}")
print(f"Volatility: {perf.calc_volatility():.2%}")
Access Trade History
# Get all trades
trades = bt.portfolio.trade_log
print(trades)
# Filter by type
buy_trades = trades[trades['Type'] == 'buy']
sell_trades = trades[trades['Type'] == 'sell']
# Summary statistics
print(f"Total trades: {len(trades)}")
print(f"Buy trades: {len(buy_trades)}")
print(f"Sell trades: {len(sell_trades)}")
print(f"Win rate: {(trades['Profit'] > 0).sum() / len(trades):.1%}")
Access Portfolio Value History
# Time series of total portfolio value
value_history = bt.portfolio.value_history
# Final value
final_value = value_history.iloc[-1]
profit = final_value - bt.initial_cap
print(f"Starting capital: ${bt.initial_cap:,.2f}")
print(f"Final value: ${final_value:,.2f}")
print(f"Profit/Loss: ${profit:,.2f}")
Visualize Results
from stocksimpy import Visualize
viz = Visualize(bt)
plt = viz.visualize_backtest()
# Show on screen
plt.show()
# Or save to file
plt.savefig("my_backtest.png", dpi=150)
Using Built-in Strategies
stocksimpy includes several ready-to-use strategies for quick testing.
Fixed-Size Strategies
1. Buy All (buy_all_fixed)
Always buy. Useful for buy-and-hold baseline comparison:
from stocksimpy import Strategy
strategy = Strategy.buy_all_fixed()
# Returns 'buy' at every step
2. RSI Momentum (rsi_momentum_fixed)
Buy on upward RSI crossover (30 → 50), sell on downward crossover (50 → 30):
strategy = Strategy.rsi_momentum_fixed(rsi_period=14)
Parameters:
rsi_period: RSI lookback window (default: 14)
3. SMA/EMA Crossover (sma_ema_crossover_fixed)
Buy when fast SMA crosses above slow SMA, sell on crossunder:
strategy = Strategy.sma_ema_crossover_fixed(fast=12, slow=26)
Parameters:
fast: Fast moving average window (default: 12)slow: Slow moving average window (default: 26)
4. RSI Reversion (rsi_reversion_fixed)
Buy when RSI is oversold, sell when overbought:
strategy = Strategy.rsi_reversion_fixed(
rsi_period=14,
low_th=30, # Oversold threshold
high_th=70 # Overbought threshold
)
5. Multi-Indicator (multi_indicator_fixed)
Buy when RSI < 60 AND price > 200-day SMA, sell when RSI > 70 OR price < SMA:
strategy = Strategy.multi_indicator_fixed(rsi_period=14, sma_long=200)
Dynamic-Size Strategies
1. Price Action (price_action_dynamic)
Buy when price drops ≥ 14% from 30 days ago, allocates $20k per trade:
strategy = Strategy.price_action_dynamic()
Creating Custom Strategies
Template: Fixed-Size Strategy
A fixed-size strategy receives historical data and returns a signal string:
def my_fixed_strategy(data):
"""
Custom fixed-size strategy.
Parameters
----------
data : pandas.DataFrame
Historical OHLCV data indexed by date.
Returns
-------
str
One of 'buy', 'sell', or 'hold'.
"""
# Extract the close price
close = data['Close']
# Your logic here
if close.iloc[-1] > close.iloc[-20]:
return 'buy'
elif close.iloc[-1] < close.iloc[-5]:
return 'sell'
else:
return 'hold'
Template: Dynamic-Size Strategy
A dynamic-size strategy receives historical data and current holdings, returns signal and share count:
def my_dynamic_strategy(data, holdings):
"""
Custom dynamic-size strategy.
Parameters
----------
data : pandas.DataFrame
Historical OHLCV data indexed by date.
holdings : float
Current shares owned.
Returns
-------
tuple
(signal, shares) where signal is 'buy', 'sell', or 'hold'
and shares is an integer number of shares to trade.
"""
close = data['Close']
if close.iloc[-1] > close.rolling(50).mean().iloc[-1]:
# Buy: allocate $10,000
shares = int(10000 / close.iloc[-1])
return 'buy', shares
elif holdings > 0 and close.iloc[-1] < close.rolling(10).mean().iloc[-1]:
# Sell all
return 'sell', holdings
else:
return 'hold', 0
Using Your Strategy
from stocksimpy import Backtester, StockData
data = StockData.from_yfinance(["AAPL"], days_before=365)
# Fixed-size
bt_fixed = Backtester("AAPL", data, my_fixed_strategy)
bt_fixed.run_backtest_fixed()
# Dynamic-size
bt_dynamic = Backtester("AAPL", data, my_dynamic_strategy)
bt_dynamic.run_backtest_dynamic()
Pro Tips
Handle MultiIndex data: The data might have MultiIndex columns for multi-ticker data. Extract close carefully:
try: close = data.loc[:, ('Close', symbol)] # MultiIndex except: close = data['Close'] # Single-level
Avoid lookahead bias: Only use data up to the current index. Don’t use
data.iloc[-1]then apply shifts; design your calculations properly.Handle edge cases: Check if you have enough data before using rolling windows:
if len(data) < 50: return 'hold'
Keep it readable: Clear variable names and comments make backtests easier to debug:
rsi = compute_rsi(data['Close'], period=14) is_oversold = rsi.iloc[-1] < 30 if is_oversold: return 'buy'
Next Steps
Explore the examples: Check the
examples/folder for complete working notebooks.Read the API docs: Visit the full API documentation for detailed method signatures and parameters.
Test multiple strategies: Compare different strategies on the same data:
from stocksimpy import Backtester, Performance, StockData, Strategy data = StockData.from_yfinance(["AAPL"], days_before=365) strategies = [ Strategy.buy_all_fixed(), Strategy.rsi_momentum_fixed(), Strategy.sma_ema_crossover_fixed(), ] for strat in strategies: bt = Backtester("AAPL", data, strat) bt.run_backtest_fixed() perf = Performance(bt) print(perf.generate_risk_report())
Optimize parameters: Use grid search or random search to find optimal parameter combinations.
Handle real-world data: Load your own CSV, database, or yfinance data and test on it.
Contribute: Found a cool strategy? Have an idea? Check CONTRIBUTING to contribute back!
Troubleshooting
“ModuleNotFoundError: No module named ‘yfinance’”
Install yfinance: pip install yfinance
“ValueError: Input DataFrame must have a ‘Date’ column or a DatetimeIndex”
Ensure your data has a properly indexed date column. If using a CSV:
df = pd.read_csv("file.csv", index_col="Date", parse_dates=True)
data = StockData(df)
“ValueError: DataFrame contains NaN values”
Your data has missing values. Fill them:
df = df.fillna(method='ffill') # Forward fill
Strategy returns no trades
Check:
Your signal logic (is it ever returning ‘buy’ or ‘sell’?)
Your data has enough rows (some strategies need lookback periods)
Transaction fees aren’t eating all capital (try lowering fees)
Performance metrics are NaN
This usually means:
The backtest didn’t execute any trades (no portfolio value change)
Not enough data to compute metrics
Ensure
run_backtest_fixed()orrun_backtest_dynamic()was called
Additional Resources
Contributing: See CONTRIBUTING
Code of Conduct: See CODE_OF_CONDUCT
Happy backtesting! 🚀