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