Skip to main content

Stock Trading Backtest Framework

A Python framework for backtesting trading strategies with support for multiple order types and technical indicators. Licensed under the LGPL.

Email ben567755 [at] gmail[.]com with questions


Install

pip install pyBacktest

Docs can be found in the wiki tab. Docs are a work in progress.


Features

Trading Operations

  • 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

Technical Indicators

  • 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

Portfolio Management

  • 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.)

Example

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

Diagrams are not guaranteed to be up to date.

Structure

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

Backtest callchain

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

Components

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]

State Diagram

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
    }

Data Flow

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]

Interaction Overview

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

Error Handling

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

Dependincies

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]

Metadata

Release files for pyBacktest 1.1.8

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pyBacktest 1.1.8
File Size Uploaded
pybacktest-1.1.8.tar.gz 14.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pyBacktest 1.1.8
File Interpreter ABI Platform
pybacktest-1.1.8-py3-none-any.whl Python 3 none any Details

Total release size: 28.8 kB

Release files / pybacktest-1.1.8.tar.gz

Download URL pybacktest-1.1.8.tar.gz
Size 14.6 kB
Tags Source
SHA-256 checksum
How to use checksums
2cfb6380f389753c536e8b408c90b442a519cf7656338b5295c06a451427bd04
BLAKE2b-256 checksum
How to use checksums
58f30c9f988188addcc5e59ca402c73406ff3b240a481cca1357a31c25e5bf08
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.1.0 CPython/3.9.21

Release files / pybacktest-1.1.8-py3-none-any.whl

Download URL pybacktest-1.1.8-py3-none-any.whl
Size 14.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
5886d75a553af274d8e3ddb10368789bc4f979b5e04be85010948b39ca601c53
BLAKE2b-256 checksum
How to use checksums
23d899a0ccb9f683d8c75cb29a73f4911d41519e61a013e63d137e36a203e910
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.1.0 CPython/3.9.21

Release history Release notifications | RSS feed

This release

1.1.8 This release

2 release files

1.1.5

2 release files

1.1.4

2 release files

1.1.3

2 release files

1.1.0

2 release files

1.0.1

2 release files

0.0.8

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page