A Python framework for backtesting trading strategies with support for multiple order types and technical indicators. Licensed under the LGPL.
Docs can be found in the wiki tab. Docs are a work in progress.
- Market Orders (Buy/Sell)
- Limit Orders
- GTC (Good Till Canceled) Orders
- Short Selling (Sell and Cover Short Positions)
- Commission Handling
- Flat Fee
- Percentage
- Per Share
- Order Expiry Handling
- Automatically cancels expired orders
- Order Queue Management
- Pending Orders handled by priority queue
- Simple Moving Average (SMA)
- Exponential Moving Average (EMA)
- Relative Strength Index (RSI)
- Bollinger Bands
- MACD (Moving Average Convergence Divergence)
- Crossover Detection (SMA, EMA, MACD)
- Volume Weighted Average Price (VWAP)
- Average True Range (ATR)
- Indicator Calculation Integration
- SMA, EMA, MACD available within strategy context
- Position Tracking
- Track open positions and entry points
- Transaction History
- Records details of executed trades
- Portfolio Valuation
- Calculates current portfolio value based on market prices
- Cash Management
- Tracks available cash for trades
- Performance Metrics
- Risk metrics, returns, and other performance statistics (e.g., Sharpe ratio, Drawdown, etc.)
import pyBacktest as pbt
from pyBacktest.strategy import Strategy
from pyBacktest.utils import calculateSMA, analyzeResults, calculateMACD
from pyBacktest.tradeTypes import TradeType
from datetime import datetime
import pandas as pd
class SMACross(Strategy):
def setup(self) -> None:
self.has_position = False
self.entry_price = 0
self.sma20 = calculateSMA(self.data['Close'], 20)
self.sma50 = calculateSMA(self.data['Close'], 50)
def step(self, row: pd.Series) -> None:
if row.name not in self.sma20.index or row.name not in self.sma50.index:
return
if self.backtest.cash>row['Close'] and self.sma20[row.name] > self.sma50[row.name]:
self.has_position = True
self.entry_price = row['Close']
self.backtest.trade(TradeType.BUY, int(self.backtest.cash*0.95/row["Close"]), row['Close'], "DAY")
elif self.has_position and self.sma20[row.name] < self.sma50[row.name]:
self.has_position = False
self.backtest.trade(TradeType.SELL, self.current_position, row['Close'], row.name)
class MACDStrategy(Strategy):
def setup(self) -> None:
self.macd, self.signal, self.histogram = calculateMACD(self.data['Close'])
def step(self, row: pd.Series) -> None:
if row.name not in self.macd.index:
return
if self.histogram[row.name] > 0:
numShares = int(self.backtest.cash / row['Close'])
trade_cost = self.backtest.calculate_trade_cost(TradeType.BUY, numShares, row['Close'])
if self.backtest.cash >= trade_cost:
self.backtest.trade(TradeType.BUY, numShares, row['Close'], "DAY")
elif self.histogram[row.name] < 0 and self.backtest.getPosition() > 0:
numShares = self.backtest.getPosition()
trade_cost = self.backtest.calculate_trade_cost(TradeType.SELL, numShares, row['Close'])
if self.backtest.cash >= trade_cost:
self.backtest.trade(TradeType.SELL, numShares, row['Close'], row.name)
if __name__ == "__main__":
sma_backtest = pbt.Backtest(
strategy=SMACross(),
ticker='AAPL',
commision=1.00,
startDate=datetime(2020, 1, 1),
endDate=datetime(2024, 1, 1),
cash=10000
)
macd_backtest = pbt.Backtest(
strategy=MACDStrategy(),
ticker='AAPL',
commision=1.00,
startDate=datetime(2020, 1, 1),
endDate=datetime(2024, 1, 1),
cash=10000
)
sma_results = sma_backtest.run()
macd_results = macd_backtest.run()
analyzeResults(sma_results)
analyzeResults(macd_results)
comparison = pbt.utils.compareBacktests(sma_results, macd_results)
print("\nComparison of SMA and MACD Strategies:")
print(f"Final Value Difference: ${comparison['final_value_diff']:,.2f}")
print(f"Total Return Difference: {comparison['total_return_diff']:.2f}%")
print(f"Number of Transactions Difference: {comparison['num_transactions_diff']}")
print(f"Better Strategy: {comparison['better_strategy']}")
Diagrams are not guaranteed to be up to date.
classDiagram
class Backtest {
- strategy: Strategy
- ticker: str
- commission: float
- startDate: datetime
- endDate: datetime
- cash: float
+ run() BacktestResult
+ trade(tradeType: TradeType, numShares: int, price: float, duration: str)
+ getPosition() int
+ calculate_trade_cost(tradeType: TradeType, numShares: int, price: float) float
}
class Strategy {
# backtest: Backtest
+ setup() void
+ step(row: pd.Series) void
}
class SMACross {
+ setup() void
+ step(row: pd.Series) void
}
class MACDStrategy {
+ setup() void
+ step(row: pd.Series) void
}
Strategy <|-- SMACross
Strategy <|-- MACDStrategy
Backtest o--> Strategy
Backtest o--> TradeType
Backtest o--> Holding
Backtest o--> Transaction
Backtest o--> Order
class TradeType {
<<Enumeration>>
+ BUY
+ SELL
+ SHORT_SELL
+ STOP
+ COVER
+ GTC
+ MARKET_BUY
+ MARKET_SELL
+ SHORT_COVER
+ LIMIT_BUY
+ LIMIT_SELL
}
class Holding {
- tradeType: TradeType
- ticker: str
- commission: float
- executedSuccessfully: bool
- numShares: int
- totalCost: float
- entryPrice: float
- shortPosition: bool
}
class Transaction {
- tradeType: TradeType
- ticker: str
- commission: float
- executedSuccessfully: bool
- numShares: int
- pricePerShare: float
- totalCost: float
- date: datetime
- profitLoss: float
- notes: str
}
class Order {
- tradeType: TradeType
- ticker: str
- numShares: int
- targetPrice: float
- duration: str
- orderDate: datetime
- active: bool
- limitPrice: float
}
class Utils {
+ calculateSMA(data: pd.Series, period: int) pd.Series
+ calculateMACD(data: pd.Series, fastPeriod: int, slowPeriod: int, signalPeriod: int) tuple
+ analyzeResults(result: BacktestResult) dict
+ compareBacktests(result1: BacktestResult, result2: BacktestResult) dict
}
Backtest --> Utils
sequenceDiagram
participant User as User
participant Backtest as Backtest
participant Strategy as Strategy
participant Utils as Utils
participant TradeTypes as TradeType
participant DataLoader as DataLoader
participant Results as BacktestResult
User->>Backtest: Initialize Backtest(ticker, strategy, dates, cash)
Backtest->>DataLoader: Load Historical Data
DataLoader-->>Backtest: Return Historical Data
Backtest->>Strategy: Instantiate Strategy
Backtest->>Strategy: Call setup()
Strategy-->>Backtest: Initialize Strategy Variables
loop For each data row
Backtest->>Strategy: Call step(row)
Strategy->>TradeTypes: Create Trade (if conditions met)
TradeTypes-->>Backtest: Return Trade Details
Backtest->>Backtest: Update Portfolio & Cash
end
Backtest->>Results: Generate Backtest Results
Results-->>Backtest: Return BacktestResult Object
User->>Utils: Analyze Results(BacktestResult)
Utils-->>User: Return Metrics and Insights
User->>Backtest: Compare Strategies(optional)
Backtest->>Utils: CompareBacktests(results1, results2)
Utils-->>User: Return Comparison Summary
graph TD
A[Backtest Framework] --> B[Strategy Module]
A --> C[Utils Module]
A --> D[tradeTypes Module]
A --> E[Data Handling]
B --> F[Custom Strategies]
C --> G[Indicator Calculations]
C --> H[Risk Metrics]
E --> I[Data Loader]
E --> J[Data Validator]
D --> K[TradeType Enum]
D --> L[Order Class]
stateDiagram-v2
state Backtest {
[*] --> Initialized
Initialized --> Running : run()
Running --> Paused : pause()
Running --> Completed : all data processed
Paused --> Running : resume()
Completed --> Archived : save results
}
state Order {
[*] --> Pending
Pending --> Executed : conditions met
Pending --> Expired : time elapsed
Executed --> Completed : trade settled
Completed --> Archived : recorded in transactions
}
graph TD
A[Input Data] -->|Load| B[DataFrame]
B -->|Validate| C[Backtest Engine]
C -->|Feed| D[Strategy Execution]
D -->|Generate| E[Trades]
E -->|Record| F[Transactions]
F -->|Summarize| G[Performance Analysis]
G -->|Output| H[Results]
sequenceDiagram
participant User
participant Backtest
participant Strategy
participant Utils
User->>Backtest: Initialize Backtest
Backtest->>Utils: Load and validate data
Backtest->>Strategy: Setup
loop For each data point
Backtest->>Strategy: step()
Strategy->>Backtest: trade()
end
Backtest->>Utils: Analyze Results
Backtest->>User: Return Results
flowchart TD
A[Run Backtest] --> B[Trade Attempt]
B -->|Funds Available| C[Execute Trade]
B -->|Funds Insufficient| D[Raise InsufficientFundsError]
C -->|Valid Trade| E[Update Transactions]
C -->|Invalid Trade| F[Raise InvalidOrderError]
E --> G[Continue Backtest]
F --> G
D --> G
graph TD
A[SPDX Document: com.github.slowpoke111/pyBacktest] --> B[Numpy]
A --> C[Pandas]
A --> D[YFinance]
A --> E[Typing Extensions]
A --> F[GH Action: PyPI Publish]
A --> G[GH Action: Checkout]
A --> H[GH Action: Setup Python]
B -->|Package Manager| P1[pypi/numpy]
C -->|Package Manager| P2[pypi/pandas]
D -->|Package Manager| P3[pypi/yfinance]
E -->|Package Manager| P4[pypi/typing-extensions]
F -->|Package Manager| P5[githubactions/pypa/gh-action-pypi-publish]
G -->|Package Manager| P6[githubactions/actions/checkout]
H -->|Package Manager| P7[githubactions/actions/setup-python]