Skip to main content

Launching our Backtesting Archive

· 11 min read
@boekenbox

We’re excited to announce the launch of the backtesting archive on Gunbot.com! This tool is designed for traders who want to optimize their Gunbot strategies by analyzing comprehensive historical trading data. With the backtesting archive, you can dive deep into backtest reports, compare different strategies, and gain valuable insights to improve your trading methods.

Symbol image backtesting archive

Easy-to-Use Viewer for Backtest Reports

The backtesting archive provides a dedicated viewer for Gunbot-generated backtest reports. This intuitive interface makes it easy to analyze your backtesting results in detail. Each backtest includes a download option that has all needed settings to run your crypto trading bot in the exact same way.

Chart viewer

Visualize and Compare Results

Our detailed charts allow you to visualize all your backtesting results at a glance. The overview table lets you easily compare performance metrics across various strategies and timeframes to identify what works best.

Overview viewer

Contribute to the Backtesting Archive

Join the Community Effort

The Gunbot backtesting archive will thrive on contributions from traders like you. By sharing your backtesting results, you help build a richer, more diverse database that benefits the entire community.

How to Submit Your Data

Contributing is simple. You can submit your backtesting results in two ways:

Each submission undergoes a review process to ensure quality and accuracy. While it might take some time, your valuable insights will soon be part of this collective resource.

What’s Inside a Backtest Report?

Here’s a sneak peek at the kind of data you can explore in the backtesting archive:

  • Pair: BTC-SOL
  • Exchange: Binance
  • Fee Percentage: 0.1%
  • Time Period: From June 7, 2024, to February 24, 2056

Key Performance Metrics

  • Starting Funds: 0.1 BTC
  • Realized PnL: 0.003594 BTC
  • ROI: 3.59%
  • Sharpe Ratio: 0.56
  • Sortino Ratio: 9.72
  • Realized Profit: 0.005393 BTC
  • Realized Loss: -0.001798 BTC
  • Average PnL %: 1.78%
  • Average Profit %: 1.93%
  • Average Loss %: -5.66%
  • Volume: 0.663889 BTC
  • Buy Volume: 0.339088 BTC
  • Sell Volume: 0.324801 BTC
  • Buys: 47
  • Sells: 51
  • Trades with Profit: 50
  • Trades with Loss: 1
  • Fees Paid: 0.000663 BTC

Strategy Settings

Explore the detailed settings used in backtests, such as:

  • Initial Funds: 0.1 BTC
  • Buy Method: channelmaestro
  • Sell Method: channelmaestro
  • Profit Target %: 7.5
  • Use Auto Gain: true
  • Buy Enabled: true
  • Sell Enabled: true

These settings allow you to see exactly how strategies are configured and how they perform under different conditions.

Bonus: Guide to Mass Backtest Runs with Gunbot on Linux

This guide will help you set up and run a mass backtest script for Gunbot on a Linux system.

Prerequisites

  1. Gunbot: Ensure you have Gunbot installed and configured.
  2. Python 3.10: Make sure Python 3.10 is installed.
  3. Required Python Libraries: Install the necessary Python libraries by running:
    pip install subprocess time os signal shutil json re itertools multiprocessing

Script Setup

  1. Download the Script: Save the provided script as mass_backtest.py in your working directory.
  2. Set Up Working Directories: Ensure you have the following directory structure:
    /home/user/dev/backtesting-runs/
    ├── 1/
    ├── 2/
    └── 3/
    Each directory should contain:
    • A complete Gunbot installation
    • A unique GUI port set for each instance
    • A config.js file with "BACKFESTER": true in the bot section and preconfigured starting balances for simulator.

Configuration

Modify Paths

Adjust the paths in the script if your directory structure is different.

Trading Pairs and Months

Customize the trading_pairs and months variables according to your needs.

Pair Overrides and Trading Limit

The script will override the configuration for each pair to ensure proper backtesting. It updates:

  • BF_SINCE and BF_UNTIL: The start and end timestamps for the backtest period.
  • TRADING_LIMIT: Calculated as the available balance divided by 30.
  • MIN_VOLUME_TO_SELL: Set as 30% of the trading limit.
  • Other strategy-specific settings.

These overrides ensure each backtest runs with consistent parameters.

Using RAM Disk

Importance of RAM Disk

Using a RAM disk can significantly speed up the backtesting process by reducing read/write times. The script copies necessary files to the RAM disk, performs the backtest, and then copies the results back to the main storage.

Check Available RAM Disk Volume

Before running the script, ensure you have sufficient space in your RAM disk (/dev/shm). Check the available volume with the following command:

df -h /dev/shm

Make sure you have enough space to handle the data for your backtests. If necessary, adjust your system's RAM disk size in your system settings.

Running the Script

  1. Navigate to the Script Directory:
    cd /path/to/your/script
  2. Run the Script:
    python3 mass_backtest.py

Script Workflow

Initial Setup

The script starts by clearing any existing temporary backtesting directories in the RAM disk (/dev/shm). It then loads or initializes the task queue with all trading pairs and date ranges.

Managing Tasks

The task queue manages which trading pairs and date ranges need to be processed. The script uses a worker directory queue to handle different working directories where backtests will be run.

Running Backtests

The script launches multiple worker processes to perform backtests concurrently. Each worker:

  • Copies necessary files to the RAM disk
  • Executes the backtest using Gunbot
  • Monitors the output for completion
  • Terminates the process upon completion
  • Copies the backtesting results back to the main storage
  • Marks the task as done and updates the completed tasks file

Completion

The script waits for all worker processes to finish. It periodically saves the state of the task queue to a file, allowing you to resume if the script is interrupted.

Increasing Process Count

To increase the number of processes running backtests concurrently:

  1. Ensure Enough Working Directories:

    • You need one working directory per process. For example, if you want to run 5 processes concurrently, you should have 5 working directories:
      /home/pim/dev/backtesting-runs/
      ├── 1/
      ├── 2/
      ├── 3/
      ├── 4/
      └── 5/
    • Each directory must have a complete Gunbot installation with a unique GUI port and a config.js file.
  2. Update the Number of Processes:

    • Open the mass_backtest.py script.
    • Locate the section where the processes are started:
      processes = []
      for _ in range(3): # 3 processes
      p = Process(target=worker, args=(task_queue, working_directory_queue))
      p.start()
      processes.append(p)
    • Change the number 3 to the desired number of processes. For example, to run 5 processes concurrently, change it to:
      processes = []
      for _ in range(5): # 5 processes
      p = Process(target=worker, args=(task_queue, working_directory_queue))
      p.start()
      processes.append(p)
  3. Update the Working Directories List:

    • Update the working_directories list at the beginning of the script to match the number of processes. For example, if you want to run 5 processes, you need 5 working directories. You should update the range to range(1, 6) to match the folder count:
      working_directories = [os.path.join(base_working_directory, str(i)) for i in range(1, 6)]  # 5 working directories
    • The number 6 here is one more than the actual count because the range function in Python is inclusive of the start value but exclusive of the end value. So, range(1, 6) creates a list from 1 to 5.

Considerations

  • System Resources: Ensure your system has enough CPU and RAM to handle the increased process count. Running too many processes can lead to resource contention and slow down the overall performance.
  • RAM Disk Space: Verify that your RAM disk (/dev/shm) has enough space to accommodate the additional processes.

Monitoring Progress

  • Console Logs: The script prints logs to the console, showing the progress of each backtest.
  • Completion Reports: Backtesting reports are saved back to the respective working directories.

Script source

mass_backtest.py

import subprocess
import time
import os
import signal
import shutil
import json
import re
from multiprocessing import Process, Queue, current_process
from queue import Empty

# Define the base working directory
base_working_directory = '/home/user/dev/backtesting-runs'
working_directories = [os.path.join(base_working_directory, str(i)) for i in range(1, 4)] # 3 working directories
command = ['./gunthy-linux']

# Static list of pairs and timeframes to use
trading_pairs = [
"USDT-BTC", "USDT-ETH", "USDT-BNB", "USDT-SOL", "USDT-XRP", "USDT-DOGE", "USDT-ADA", "USDT-TRX",
"USDT-AVAX", "USDT-SHIB", "USDT-DOT", "USDT-LINK", "USDT-BCH", "USDT-NEAR", "USDT-MATIC", "USDT-LTC",
"USDT-UNI", "USDT-PEPE", "BTC-ETH", "BTC-BNB", "BTC-SOL", "BTC-XRP", "BTC-DOGE", "BTC-ADA", "BTC-TRX",
"BTC-AVAX", "BTC-DOT", "BTC-LINK", "BTC-BCH", "BTC-NEAR", "BTC-MATIC", "BTC-LTC", "BTC-UNI", "BNB-SOL",
"BNB-XRP", "BNB-ADA", "BNB-TRX", "BNB-AVAX", "BNB-DOT", "BNB-LINK", "BNB-BCH", "BNB-NEAR", "BNB-MATIC",
"BNB-LTC", "ETH-BNB", "ETH-XRP", "ETH-SOL", "ETH-ADA", "ETH-TRX", "ETH-AVAX"
]

months = [
(1704067200000, 1706659199000), # Jan 2024
(1706659200000, 1709251199000), # Feb 2024
(1709251200000, 1711843199000), # Mar 2024
(1711843200000, 1714435199000), # Apr 2024
(1714435200000, 1717027199000), # May 2024
(1717027200000, 1719619199000), # Jun 2024
]

def transform_pair(pair):
if pair.endswith('BTC'):
return f"BTC-{pair[:-3]}"
elif pair.endswith('ETH'):
return f"ETH-{pair[:-3]}"
elif pair.endswith('USDT'):
return f"USDT-{pair[:-4]}"
elif pair.endswith('BNB'):
return f"BNB-{pair[:-3]}"
return pair

def clear_ramdisk():
ramdisk_base = '/dev/shm/'
for item in os.listdir(ramdisk_base):
item_path = os.path.join(ramdisk_base, item)
if os.path.isdir(item_path) and item.startswith('backtesting_'):
shutil.rmtree(item_path)
print(f"Cleared RAM disk directory: {item_path}")

def delete_folders(working_directory):
json_folder = os.path.join(working_directory, 'json')
backtesting_folder = os.path.join(working_directory, 'backtesting')

if os.path.exists(json_folder):
shutil.rmtree(json_folder)
print(f"Deleted folder: {json_folder}")
if os.path.exists(backtesting_folder):
shutil.rmtree(backtesting_folder)
print(f"Deleted folder: {backtesting_folder}")

def read_config(working_directory):
config_path = os.path.join(working_directory, 'config.js')
with open(config_path, 'r') as file:
config_data = file.read()
config_data = re.sub(r'^module\.exports\s*=\s*', '', config_data)
config_data = re.sub(r';\s*$', '', config_data)
return json.loads(config_data)

def write_config(config, working_directory):
config_path = os.path.join(working_directory, 'config.js')
config_data = json.dumps(config, indent=4)
with open(config_path, 'w') as file:
file.write(config_data)

def ensure_pair_config(config, pair):
exchange = 'binance'
if exchange not in config['pairs']:
config['pairs'][exchange] = {}

config['pairs'][exchange] = {}
if pair not in config['pairs'][exchange]:
config['pairs'][exchange][pair] = {
"strategy": "channelmaestro",
"enabled": True,
"override": {
"INITIAL_FUNDS": "500",
"BUY_METHOD": "channelmaestro",
"SELL_METHOD": "channelmaestro",
"COMPOUND_RATIO": "1",
"COMPOUND_PROFITS_SINCE": "0",
"USE_STOP_AFTER_PROFIT": False,
"PROFIT_TARGET_PCT": "7.5",
"USE_AUTO_GAIN": True,
"GAIN_PARTIAL": "0.5",
"GAIN": "2",
"BUY_ENABLED": True,
"SELL_ENABLED": True,
"STOP_AFTER_SELL": False,
"MIN_VOLUME_TO_SELL": 10,
"MAX_INVESTMENT": "999999999999999",
"PERIOD": "5",
"PERIOD_MEDIUM": "15",
"PERIOD_LONG": "30",
"IGNORE_TRADES_BEFORE": "0",
"BF_SINCE": 0,
"BF_UNTIL": 0,
"USE_EXPERIMENTS": False
}
}

def update_config(config, pair, start, end):
if not config['bot']['BACKFESTER']:
config['bot']['BACKFESTER'] = True

base, quote = pair.split('-')[0], pair.split('-')[1]
balance = float(config['bot']['simulatorBalances']['binance'][base])

trading_limit = balance / 30
min_volume_to_sell = trading_limit * 0.3

ensure_pair_config(config, pair)

config['pairs']['binance'][pair]['override']['BF_SINCE'] = start
config['pairs']['binance'][pair]['override']['BF_UNTIL'] = end
config['pairs']['binance'][pair]['override']['TRADING_LIMIT'] = trading_limit
config['pairs']['binance'][pair]['override']['MIN_VOLUME_TO_SELL'] = min_volume_to_sell
config['pairs']['binance'][pair]['override']['INITIAL_FUNDS'] = balance

def copy_to_ram(working_directory, ram_directory):
print(f"Copying {working_directory} to {ram_directory}")
if os.path.exists(ram_directory):
shutil.rmtree(ram_directory)
shutil.copytree(working_directory, ram_directory)

def copy_backtesting_reports_from_ram(working_directory, ram_directory):
print(f"Copying backtestingReports from {ram_directory} back to {working_directory}")
ram_backtesting_reports = os.path.join(ram_directory, 'backtestingReports')
main_backtesting_reports = os.path.join(working_directory, 'backtestingReports')
if os.path.exists(ram_backtesting_reports):
if os.path.exists(main_backtesting_reports):
shutil.rmtree(main_backtesting_reports)
shutil.copytree(ram_backtesting_reports, main_backtesting_reports)

def launch_and_kill_process(command, working_directory, ram_directory):
copy_to_ram(working_directory, ram_directory)
process = subprocess.Popen(command, cwd=ram_directory, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
print(f"Process {process.pid} started in {ram_directory}.")

last_output_time = time.time()

try:
while True:
output = process.stdout.readline()
if output == '' and process.poll() is not None:
break
if output:
last_output_time = time.time()
print(output.strip())
if 'Backtesting report created successfully' in output:
break
if 'Backtester completed the job: your data will be available soon on your GUI' in output:
time.sleep(3)
break
if time.time() - last_output_time > 8:
print(f"No log output for 8 seconds, restarting process {process.pid}")
os.kill(process.pid, signal.SIGTERM)
process.wait()
process = subprocess.Popen(command, cwd=ram_directory, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
last_output_time = time.time()
finally:
if process.poll() is None:
os.kill(process.pid, signal.SIGTERM)
print(f"Process {process.pid} in {ram_directory} has been terminated.")
copy_backtesting_reports_from_ram(working_directory, ram_directory)
delete_folders(ram_directory)

def run_backtest(pair, start, end, working_directory, ram_directory, queue):
config = read_config(working_directory)
update_config(config, pair, start, end)
write_config(config, working_directory)
launch_and_kill_process(command, working_directory, ram_directory)
time.sleep(1) # Wait for 1 second before the next run
queue.put(working_directory) # Indicate that this working directory is free

def save_queue(task_queue, filename='task_queue.json'):
with open(filename, 'w') as file:
tasks = []
while not task_queue.empty():
tasks.append(task_queue.get())
json.dump(tasks, file)
for task in tasks:
task_queue.put(task)

def load_queue(filename='task_queue.json'):
task_queue = Queue()
if os.path.exists(filename):
with open(filename, 'r') as file:
tasks = json.load(file)
for task in tasks:
task_queue.put(tuple(task))
else:
for pair in trading_pairs:
for start, end in months:
task_queue.put((pair, start, end))
return task_queue

def worker(task_queue, working_directory_queue, completed_tasks_filename='completed_tasks.json'):
completed_tasks = []

if os.path.exists(completed_tasks_filename):
with open(completed_tasks_filename, 'r') as file:
completed_tasks = json.load(file)

while True:
try:
pair, start, end = task_queue.get(timeout=5) # timeout added to allow graceful shutdown
except Empty:
break

if (pair, start, end) in completed_tasks:
continue

working_directory = working_directory_queue.get()
ram_directory = os.path.join('/dev/shm', f'backtesting_{current_process().pid}')

print(f"Worker {current_process().pid} processing {pair} from {start} to {end} in {working_directory}")
run_backtest(pair, start, end, working_directory, ram_directory, working_directory_queue)

completed_tasks.append((pair, start, end))
with open(completed_tasks_filename, 'w') as file:
json.dump(completed_tasks, file)

if __name__ == "__main__":
clear_ramdisk()

task_queue = load_queue()
working_directory_queue = Queue()
save_queue(task_queue)

for wd in working_directories:
working_directory_queue.put(wd)

processes = []
for i in range(3): # 3 processes
p = Process(target=worker, args=(task_queue, working_directory_queue))
p.start()
processes.append(p)
time.sleep(0.5) # Introduce a small time offset of 0.5 seconds before starting the next process

for p in processes:
p.join()

save_queue(task_queue)

Conclusion

The new backtesting archive feature on Gunbot.com is a powerful tool for traders looking to refine their strategies with precision. By utilizing and contributing to this resource, you can continuously improve your trading methods and share valuable insights with the Gunbot community. Start exploring the backtesting archive today and take your trading to the next level!