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.
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.
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.
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:
- GitHub: Create a pull request on our GitHub repository:
- Telegram: Drop your files in our dedicated Telegram group.
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β
- Gunbot: Ensure you have Gunbot installed and configured.
- Python 3.10: Make sure Python 3.10 is installed.
- 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. - Set Up Working Directories: Ensure you have the following directory structure:Each directory should contain:
/home/user/dev/backtesting-runs/
βββ 1/
βββ 2/
βββ 3/- 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
andBF_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β
- 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.
- You need one working directory per process. For example, if you want to run 5 processes concurrently, you should have 5 working directories:
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)
- Open the
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 torange(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 therange
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.
- Update the
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!