Skip to main content

A Python backtesting framework for quantitative trading strategies

Project description

StratCraft

PyPI version Python License: MIT

A Python backtesting framework for building and evaluating quantitative trading strategies. StratCraft provides a clean, decorator-based API for defining indicators, handling multi-symbol data, and running event-driven backtests with portfolio tracking and performance analytics.

Features

  • Event-driven backtesting — step-by-step simulation with clean separation of initialize() and step_forward() hooks
  • Multi-symbol support — buy/sell across multiple tickers in a single call
  • Decorator toolkit@broadcast, @rolling, @grouping, @available for composable indicator computation
  • Portfolio management — track cash, equity, open positions, stop-loss and take-profit triggers
  • Performance metrics — Sharpe ratio, Sortino ratio, Calmar ratio, max drawdown, win rate, profit factor, and more
  • Interactive charts — Plotly-based equity curve with trade entry markers and optional additional panels
  • Data screening — filter and rank symbols by criteria within step_forward()

Installation

Install from PyPI:

pip install stratcraft

Or install from source:

git clone https://github.com/kuanhungwang/stratcraft.git
cd stratcraft
pip install -e .

Quick Start

from stratcraft import Strategy, DataHandler
from datetime import datetime

class SMAStrategy(Strategy):
    def initialize(self):
        # Load price data and compute indicators here
        self.data = {
            'open':  open_df,
            'high':  high_df,
            'low':   low_df,
            'close': close_df,
        }
        self.data['sma20'] = close_df.rolling(20).mean()

    def step_forward(self, data: DataHandler):
        close = data['close']      # latest close (scalar or Series)
        sma20 = data['sma20']

        if close > sma20:
            self.buy(symbol='AAPL', value=10_000)
        else:
            self.sell(symbol='AAPL', value=10_000)

strategy = SMAStrategy(initial_capital=100_000)
strategy.run(
    start_date=datetime(2022, 1, 1),
    end_date=datetime(2023, 12, 31),
    data_length=2,   # rows of history passed to step_forward
)

Core Concepts

Strategy lifecycle

Method When called Purpose
initialize() Once before the loop Load data, compute indicators, set parameters
step_forward(data) Every trading day Implement trading logic, call buy()/sell()

Buying and selling

# By quantity
self.buy(symbol='AAPL', quantity=100)
self.sell(symbol='AAPL', quantity=100)

# By dollar value
self.buy(symbol='AAPL', value=10_000)

# With stop-loss and take-profit (percentage of entry price)
self.buy(symbol='AAPL', quantity=100, stop_loss_percent=5, take_profit_percent=10)

# Multiple symbols at once
self.buy(symbol=['AAPL', 'MSFT'], value=[5_000, 5_000])

Accessing historical data inside step_forward

def step_forward(self, data: DataHandler):
    latest_close  = data['close']           # latest bar
    prev_close    = data[('close', -2)]     # bar before latest
    latest_sma    = data['sma20']

Set data_length in strategy.run() to the maximum look-back depth you need.

Trailing stop-loss

from stratcraft import TrailingStopLoss, Direction

trade = self.buy(symbol='AAPL', quantity=100)
if trade:
    trade.stop_loss = TrailingStopLoss(
        price=trade.entry_price - 1.0,   # initial stop price
        distance=1.0,                     # trail distance
        threshold=trade.entry_price + 1.0,
        direction=Direction.LONG,
    )

Call trade.stop_loss.reset_price(current_price) each bar to move the stop up.

Screening and ranking symbols

def step_forward(self, data: DataHandler):
    # Filter symbols passing all boolean criteria
    candidates = data.screen(['criteria_momentum', 'criteria_volume'])

    # Rank by a field, optionally from a filtered subset
    top5 = data.highest('momentum_score', n=5, tickers=candidates)
    bot5 = data.lowest('volatility', n=5)

Prefix keys with signal_, criteria_, screen_, or filter_ in self.data — StratCraft will warn you during run() if any never triggered.

Portfolio queries inside step_forward

cash         = self.portfolio.cash
equity       = self.portfolio.equity
live         = self.portfolio.live_trades()
cost_aapl    = self.portfolio.cost(symbol='AAPL')
mkt_val      = self.portfolio.current_market_value()
invest_ratio = self.portfolio.invest_ratio()
days         = self.days_since_last_trade()

Decorators

Decorators live in stratcraft.decorators and are designed to be composed.

@broadcast

Applies a single-symbol (Series) function across all columns of a DataFrame:

from stratcraft.decorators import broadcast
import ta

@broadcast
def RSI(price):
    return ta.momentum.RSIIndicator(price).rsi()

rsi = RSI(close_df)   # returns DataFrame with same columns as close_df

@rolling

Turns a scalar reduction into a rolling-window Series/DataFrame:

from stratcraft.decorators import rolling

@rolling(window=14)
def avg_range(high, low):
    return (high - low).mean()

atr = avg_range(high_df, low_df)

Compose with @broadcast for multi-symbol rolling indicators:

@broadcast
@rolling(window=20)
def momentum(price):
    return (price.iloc[-1] / price.iloc[0]) - 1

@grouping

Applies a function to user-defined groups of symbols and returns a group-level DataFrame:

from stratcraft.decorators import grouping

sector = {'Technology': ['AAPL', 'MSFT'], 'Finance': ['JPM', 'BAC']}

@grouping(groups=sector)
def sector_return(price):
    return price.pct_change().mean(axis=1)

sector_ret = sector_return(close_df)  # columns: Technology, Finance

@available

Aligns low-frequency data (e.g. quarterly earnings) to a daily time series, exposing only data that would have been available on each date:

from stratcraft.decorators import available, broadcast

@broadcast
@available(looping_dates=close.index, length=1)
def daily_eps(eps):
    return eps.iloc[-1]

eps_daily = daily_eps(fundamental['is_eps'], available_date=fundamental['fillingDate'])
pe = close / eps_daily

Performance Analysis

from stratcraft.metrics import Metrics

trade_df  = strategy.portfolio.trade_history()
pl_df     = strategy.portfolio.pl_history()

m = Metrics(trade_df, pl_df)

# Full metrics dict
results = m.metrics()

# Concise subset: cumulative return, annual return, win rate, volatility, Sharpe, max drawdown, # trades
results = m.metrics(concise=True)

Metrics.pretty_print(results)

Metrics computed:

Metric Description
Cumulative returns Total return over the period
Annual return Annualised CAGR
Annual volatility Annualised std dev of daily returns
Sharpe ratio Annual return / annual volatility
Sortino ratio Annual return / downside deviation
Calmar ratio Annual return / max drawdown
Max drawdown Largest peak-to-trough decline
Omega ratio Sum of gains / sum of losses above threshold
Stability R² of linear fit on cumulative returns
Win rate Fraction of closed trades that are profitable
Profit factor Total wins / total losses
Avg win / loss trade p/l Mean P&L of winning/losing trades
Skew / Kurtosis Distribution shape of daily returns
Tail ratio 95th percentile return / 5th percentile return

Interactive chart

fig = m.chart_history()
fig.show()

# With additional panels
fig = m.chart_history(additional_chart_data={
    'price_data':  {'AAPL': aapl_df},
    'indicators':  {'RSI': rsi_series, 'SMA20': sma_series},
    'cash_ratio':  pl_df['cash'] / pl_df['equity'],
})
fig.write_html('backtest.html')

Project Structure

stratcraft/
├── stratcraft.py      # Core: Strategy, Portfolio, Trade, MarketHandler, DataHandler
├── decorators.py      # @broadcast, @rolling, @grouping, @available
├── metrics.py         # Metrics class with performance analytics and Plotly charts
├── util.py            # Helper utilities (case-insensitive access, date range, symbol alignment)
├── examples.py        # API usage reference
└── examples/          # Complete runnable strategy examples
    ├── strategy1_single_stock_technical.py
    ├── strategy2_multi_stock_technical.py
    ├── strategy3_multi_position_technical.py
    ├── strategy4_single_stock_technical_fundamental.py
    ├── strategy5_multi_stock_technical_fundamental.py
    └── strategy6_comparing_index.py

Requirements

  • Python >= 3.10
  • pandas >= 1.5
  • numpy >= 1.23
  • plotly >= 5.0

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

stratcraft-0.1.1.tar.gz (33.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

stratcraft-0.1.1-py3-none-any.whl (31.3 kB view details)

Uploaded Python 3

File details

Details for the file stratcraft-0.1.1.tar.gz.

File metadata

  • Download URL: stratcraft-0.1.1.tar.gz
  • Upload date:
  • Size: 33.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.7

File hashes

Hashes for stratcraft-0.1.1.tar.gz
Algorithm Hash digest
SHA256 e7d64705ce8f88c8167732d9e90fffc23fc42f5833d89b3d8848a8836c8824e7
MD5 dc4f82fcb422dcca5640f66622f2d8b0
BLAKE2b-256 365cb72fae65fa1bdfba84d4dec3b7244c9c811ca7488e394c424a7c363d3967

See more details on using hashes here.

File details

Details for the file stratcraft-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: stratcraft-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 31.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.7

File hashes

Hashes for stratcraft-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 26eafeff283a6165bbec54da19ef36912f468de5850f374bc50501d487d4a233
MD5 8c4cf7848dcf47d36c848941ca04dd6f
BLAKE2b-256 5f0ebd745e0c7da0532e77173387237a4275bee5682697e7d4ee0a2ba2778648

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page