GitHub - samhuyc/Backtester: Homemade modular backtester for trading · GitHub
Skip to content

About

Homemade modular backtester for trading

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

45 Commits

Folders and files

Repository files navigation

Modular Trading Strategy Backtester

repository for 46-902 final project

Yichen (Sam) Hu

andrewid: samhu

Project Overview

  • This backtester module enables users to flexibly test out their trading strategies by easily swapping Signal, Exec, Eval components.

    • Imagine the Signal module as a quantitative researcher, who uses past market data to provide their estimate about where future prices will be.
    • Imagine the Exec module as a trader, who utilizes the future price estimate to buy/sell the assets at an optimal quantity and moment.
    • Imagine the Eval module as a portfolio manager, who looks over the past trading records and comprehensively analyze how well the team is doing, beyond just profit and losses.
  • This backtester contain numerous built-in Sign, Exec, Eval for users to experiment with. The API for these components are also Dev-friendly, and users can create their own components at ease!

  • This backtester support multipler trading strategies to be tested concurrently over a universe of tickers -- so users can test the optimal setup to form a viable strategy.

  • This backtester supports per-strategy and across-strategy trade summary, evaluation, visualization, and local storage -- so users can get the most useful feedback from a backtest.

  • This backtester by default uses Yahoo Finance historical price series, but it will support user-provided dataset in the future.

Running the Project

  1. Start by ensuring your dependencies are up to date by pip install uv and run uv sync
  2. In your python script, simply follow the below approach to begin backtesting!:

A. Basic usage:

from backtester.engine import BacktestEngine

mybt = BacktestEngine(tickers='TSLA', start='2020-01-01', end='2025-01-01')

from backtester.trader import Trader
from backtester.signal import LastPriceChange
from backtester.execution import DirectionalExec
from backtester.eval import PnL, SharpeRatioCash, MaxDrawdown

trader = Trader(signal=LastPriceChange(tickers='TSLA'), 
                execution=DirectionalExec(tradeable_tickers='TSLA'), 
                eval=[PnL(), SharpeRatioCash(), MaxDrawdown()])

mybt.add_trader(trader)
mybt.run()
mybt.summary()

You should expect the following output in your CLI:

Backtesting: 100%|████████████████████████| 1258/1258 [00:01<00:00, 1048.52it/s]
============================================================
Trader 1
  Signal:    LastPriceChange
  Execution: DirectionalExec; market orders with atclose filling
  Eval:      ['PnL', 'SharpeRatioCash', 'MaxDrawdown']
  Final timestamp: 2024-12-31 00:00:00
  Final cash:      3660595.34
  Final positions:
    TSLA: -4802.2181
  Final eval metrics:
    PnL: 1721267.6123
    SharpeRatioCash: 0.0402
    MaxDrawdown: 810270.2322

B. Tuning your Signal (see details of various signals you can choose from and their respective param schemes in signal.py):

mybt = BacktestEngine(tickers='TSLA', start='2020-01-01', end='2025-01-01')

# traderA: exact same as above
traderA = Trader(signal=LastPriceChange(tickers='TSLA'), 
                execution=DirectionalExec(tradeable_tickers='TSLA'), 
                eval=[PnL(), SharpeRatioCash(), MaxDrawdown()])
mybt.add_trader(traderA)

# traderB: same stategy as A, but tuned behavior
traderB = Trader(signal=LastPriceChange(tickers='TSLA', 
                                        confidence_max=0.8,
                                        visibility=20), 
                execution=DirectionalExec(tradeable_tickers='TSLA', 
                                            sensitivity=0.5), 
                eval=[PnL(), SharpeRatioCash(), MaxDrawdown()])
mybt.add_trader(traderB)

Different parameters can lead to different performances:

Backtesting: 100%|█████████████████████████| 1258/1258 [00:02<00:00, 534.91it/s]
============================================================
Trader 1
  Signal:    LastPriceChange
  Execution: DirectionalExec; market orders with atclose filling
  Eval:      ['PnL', 'SharpeRatioCash', 'MaxDrawdown']
  Final timestamp: 2024-12-31 00:00:00
  Final cash:      3660595.34
  Final positions:
    TSLA: -4802.2181
  Final eval metrics:
    PnL: 1721267.6123
    SharpeRatioCash: 0.0402
    MaxDrawdown: 810270.2322

============================================================
Trader 2
  Signal:    LastPriceChange
  Execution: DirectionalExec; market orders with atclose filling
  Eval:      ['PnL', 'SharpeRatioCash', 'MaxDrawdown']
  Final timestamp: 2024-12-31 00:00:00
  Final cash:      5582682.83
  Final positions:
    TSLA: -9123.3121
  Final eval metrics:
    PnL: 1898324.4899
    SharpeRatioCash: 0.0305
    MaxDrawdown: 965469.4280

C. Show metric-specific summary:

You can also show visualizations of trader's performance across selected metrics

mybt.run()
mybt.eval_summary()

f1

D. Run parameter search across different strategies over a list of tickers:

tickers = ['TSLA', 'NVDA', 'AAPL', 'META']
mybt = BacktestEngine(tickers= tickers, start='2023-01-01', end='2025-01-01')

# first group of traders: runs exponentially weighted mean reversion
from backtester.signal import ExponentialMeanReversion
from backtester.execution import DirectionalExec
for decay in np.linspace(0.5, 1, 10):
    trader = Trader(signal=ExponentialMeanReversion(tickers=tickers, decay=decay), 
                    execution=DirectionalExec(tradeable_tickers=tickers), 
                    eval=[PnL(), ])
    mybt.add_trader(trader, verbose=verbose)

# second group of traders: run bar-midpoint based fair price signal with a different Exec subclass
from backtester.signal import BarMA
from backtester.execution import EdgeExec
for windowsize in range(10, 110, 10):
    trader = Trader(signal=BarMA(tickers=tickers, window = windowsize), 
                    execution=EdgeExec(tradeable_tickers=tickers, order_style='market', fill_style='immediate'), 
                    eval=[PnL(), ])
    mybt.add_trader(trader, verbose=verbose)

mybt.run()
mybt.eval_summary()

f2

E. Save backtest runs:

mybt.save_logs('./my_folder')
Saved logs for Trader 1 to ./test_output_presentation/trader_1_logs.csv
Saved logs for Trader 2 to ./test_output_presentation/trader_2_logs.csv
Saved logs for Trader 3 to ./test_output_presentation/trader_3_logs.csv
...

F. Check out the demo video:

https://youtu.be/zyM4ovGhtp4

About

Homemade modular backtester for trading

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages