We’re excited to announce the Backtesting Archive on Gunbot.com! This tool helps traders optimize their strategies by analyzing historical trading data. Plus, we’ve included a ready-made script to run mass backtests efficiently.

With the backtesting archive, you can explore detailed backtest reports, compare strategies, and get insights to improve your trading methods.

Easy-to-Use Backtest Viewer

The Backtesting Archive comes with a dedicated viewer for Gunbot-generated backtest reports. This user-friendly interface allows you to analyze your backtesting results thoroughly. Each backtest includes a download option with all the settings needed to run your bot the same way.

Visualize and Compare Your Results

Our detailed charts help you visualize all your backtesting results quickly. The overview table lets you easily compare performance metrics across various strategies and timeframes to identify the best ones.

Contribute to the Backtesting Archive

Join the Community Effort

The Gunbot Backtesting Archive benefits from contributions from traders like you. By sharing your backtesting results, you help build a more comprehensive database that benefits the whole community.

How to Submit Your Data

Submitting your backtesting results is easy:
– **GitHub**: Create a pull request on our GitHub repository
– **Telegram**: Drop your files in our dedicated Telegram group

Each submission is reviewed for quality and accuracy. Though it might take some time, your insights will soon be part of this valuable resource.

What’s Inside a Backtest Report?

Here’s what 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

See how strategies are configured with these settings:
– 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

Guide to Mass Backtest Runs on Linux

Want to run mass backtests efficiently? Check out our guide to setting up and running a mass backtest script for Gunbot on a Linux system. Here’s a quick teaser of the steps involved:

Prerequisites

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

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 the simulator.

Modify Paths and settings

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.

Using 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

Navigate to the Script Directory:

cd /path/to/your/script

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:

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.

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:
python
processes = []
for _ in range(5): # 5 processes
p = Process(target=worker, args=(task_queue, working_directory_queue))
p.start()
processes.append(p)

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

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.exportss*=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!

If you found this article helpful, please give it some claps to let us know!

Introducing Gunbot’s Backtesting Archive was originally published in Mastering Gunbot: Tips and Tutorials from gunbot.com on Medium, where people are continuing the conversation by highlighting and responding to this story.

Mastering Gunbot: Tips and Tutorials from gunbot.com – Medium 

Read More 

Next

Leave a Reply

Your email address will not be published. Required fields are marked *