Skip to content

kuanHungWang/Stratcraft

Repository files navigation

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

About

A Python backtesting framework for building and evaluating quantitative trading strategies. Supports multiple assets.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages