How-To: Create self-adjusting, auto-compounding multi-pair DCA crypto trading bot using Python, AWS Lambda & 3Commas API

The following article describes how we can leverage Python & AWS Lambda & 3Commas API to automatically adjust the Base and Safety order size of a DCA crypto trading multi-pair bot based on the total balance available in the account.

Prerequisites

  • AWS Account
  • 3Commas Account
  • 3Commas API read/write keys
  • Trading Account supported by 3Commas (Coinbase, FTX, etc.)

 

Python Script + Dependencies

Dependencies:

  • cctx
  • certifi
  • charset_normalizer
  • idna
  • py3cw
  • requests

Packaged Python lambda function + all dependencies can be downloaded from GitHub: https://github.com/JozefJarosciak/3commas-python-selfadjusting-dca-bot

 

Problem Definition

Until now, most of my playing with the crypto market was limited to writing my own Python trading bots and I always leveraged Splunk and Splunk API to create my own charts and prediction models, which I plugged back into my bots and in such way adjusted my trading strategies using machine learning. However, recently I came across the 3Commas DCA bots and I kind of like what they were doing with the product and started using it with good success on their paper trading accounts.

Now, to the issue at hand. My biggest pet peeve with 3Commas is that they do not support automatic adjusting of Base and Safety order sizes based on the total balance of the account in multi-pair bots. The % sizing option is only available for single pair bots:

3Commas, at the moment, does not offer any logic to automatically adjust these values for multi-pair DCA bots, and that means, that as time goes and your trading account balance grows larger, you will need to periodically visit and adjust your multi-pair DCA bots to make sure you’re leveraging the power of compounding. For example, if your trading account closes 50 deals a day, you’ll essentially need to login to the 3commas website 50 times a day and adjust these two values, otherwise, you’ll be losing money. Why? Well, compounding is essentially money multiplying itself, meaning, more money you have for your DCA bot to trade with, the more money you can assign to each trade and that resolves in larger profits from your trades. So, it would be a huge mistake not to earn more profit just because 3Commas does not allow base and safety orders to be based on the percentage of the total balance of your account.

These are the two settings that I am referring to:

To automatically adjust these two values, we need to create a program, that is smart enough to log in to 3commas, look up the total value of the USDT balance, and then use it to recalculate the base and safety order size and adjust the DCA bot to change the values in your strategy. In order to do so, it needs to calculate these values in such a way, that it gets as close to the total as possible, meaning it has to do the calculation for each step in the trade and bring it as close as possible to the total amount.

So, let’s say we have the following DCA bot:

  • An account with the balance of 10,000 USDT
  • We want to trade 10 USDT pairs (ADA, 1INCH, AAVE …) using a long strategy
  • Taking profit in Quote (USDT) currency
  • Max safety trades count: 10
  • Max active safety trades count: 5
  • Price deviation to open safety orders (% from initial order): 2%
  • Safety order volume scale: 1
  • Safety order step scale: 1

How would you calculate the Base and Safety order sizes?

Well, one way is to manually keep trying, until you get close to the total of 10,000 USDT that is available for trading. You’d probably spent several minutes doing this and you’d arrive at one of the possible solutions, that the best option is to use:

  • Base order size: USDT 59
  • Safety order size: USDT 94

The calculation for all 10 safety trades would look like this:

Order № Deviation, % Order Price Average price Required price Required change, % Total
Size (ADA) Volume (USDT) Size (ADA) Volume (USDT)
BO 0 30.2 59.192 1.96 1.96 1.993 1.68367346 30.2 59.192
1 2 49 94.08000001 1.92 1.93525252 1.968 2.5 79.2 153.27200001
2 4 50 94.05000001 1.881 1.91425696 1.946 3.45560871 129.2 247.32200002
3 6 51.1 94.1262 1.842 1.89377814 1.925 4.50597176 180.3 341.44820002
4 8 52.2 94.1166 1.803 1.87339698 1.905 5.65723793 232.5 435.56480002
5 10 53.3 94.0212 1.764 1.8529951 1.884 6.80272108 285.8 529.58600002
6 12 54.6 94.1304 1.724 1.83230434 1.863 8.06264501 340.4 623.71640002
7 14 55.8 94.02300001 1.685 1.8115583 1.842 9.31750741 396.2 717.73940003
8 16 57.2 94.1512 1.646 1.79067181 1.821 10.63183475 453.4 811.89060003
9 18 58.5 94.00950001 1.607 1.76968177 1.799 11.94772868 511.9 905.90010004
10 20 60 94.08000001 1.568 1.74852264 1.778 13.39285714 571.9 999.98010005

This setting would use 99.99% of your available account or 9998 USDT.

Now, this is great, but what if you’ve just closed several trades and generated an additional 110 USDT towards the base balance. Now you have 10,110 USDT in your account, but your DCA bot is still using only 10,000 USDT.

To fix it, and leverage an additional 11% USDT available in your account, you’d have to log in to 3Commas and manually adjust the base and safety order size. After a bit of calculation you’d arrive at the following change:

  • Base order size: USDT 60 (increased from 59)
  • Safety order size: USDT 95 (increased from 94)

The safety order calculation table would change now to look like this. As you can see, each safety trade would now leverage the additional 110 USDT available in the account, meaning you’d earn more money from your trades:

Order № Deviation, % Order Price Average price Required price Required change, % Total
Size (ADA) Volume (USDT) Size (ADA) Volume (USDT)
BO 0 30.7 60.17200001 1.96 1.96 1.993 1.68367346 30.7 60.17200001
1 2 49.5 95.04 1.92 1.93531172 1.968 2.5 80.2 155.21200001
2 4 50.6 95.1786 1.881 1.91430122 1.946 3.45560871 130.8 250.39060001
3 6 51.6 95.0472 1.842 1.89384758 1.926 4.56026058 182.4 345.43780001
4 8 52.7 95.01810001 1.803 1.87348319 1.905 5.65723793 235.1 440.45590002
5 10 53.9 95.0796 1.764 1.85306401 1.884 6.80272108 289 535.53550002
6 12 55.2 95.16480001 1.724 1.83236577 1.863 8.06264501 344.2 630.70030003
7 14 56.4 95.034 1.685 1.81161832 1.842 9.31750741 400.6 725.73430003
8 16 57.8 95.13880001 1.646 1.79073538 1.821 10.63183475 458.4 820.87310004
9 18 59.2 95.1344 1.607 1.76972082 1.799 11.94772868 517.6 916.00750004
10 20 60.6 95.0208 1.568 1.74857886 1.778 13.39285714 578.2 1011.02830004

 

Solution Architecture

Envisioned architecture:

 

Python Source Code

I will not go down to the details of how the below code works, because that would increase the size of this article beyond the scope of my free time, but I’ve created the below Python script, which allows you to configure the bot, and it finds the best value for the base and safety order size and automatically login to 3Commas and adjust these values in your existing DCA bot.

Packaged Python lambda function + all dependencies can be downloaded from GitHub: https://github.com/JozefJarosciak/3commas-python-selfadjusting-dca-bot

You can see the code of the lambda function here: https://github.com/JozefJarosciak/3commas-python-selfadjusting-dca-bot/blob/main/lambda_function.py

This is the raw format:

import os
from py3cw.request import Py3CW

def lambda_handler(event, context):

####################
# 3c configuration #
####################
threecommas_key = os.environ['key']
threecommas_secret = os.environ['secret']

#####################
# test run only? #
#####################
test_run = 'no'

#####################
# bot configuration #
#####################
account_name = 'Paper Account' # your account name
bot_name = 'Paper_DCA_Bot' # your bot name
bot_id = 'xxxxxxx' # 3commas ID of the bot you want to auto calculate the Base and Safety orders - get it from the bot url (https://3commas.io/bots/xxxxxxx/edit)
bot_take_profit = '1.7'
bot_trailing_enabled = True
bot_trailing_deviation = '0.5'
bot_max_safety_orders_count = 25
bot_max_active_deals = 6
bot_active_safety_orders_count = 1
bot_pairs = ['USDT_AAVE', 'USDT_AKRO', ...] # list your pairs here
bot_safety_order_size = int(2)
bot_strategy_list = [{'options': {'time': '1m', 'type': 'strong_buy'}, 'strategy': 'trading_view'}, {'options': {'time': '5m', 'type': 'strong_buy'}, 'strategy': 'trading_view'}, {'options': {'time': '15m', 'type': 'strong_buy'}, 'strategy': 'trading_view'}, {'options': {'time': '1h', 'type': 'strong_buy'}, 'strategy': 'trading_view'}, {'options': {'time': '4h', 'type': 'buy_or_strong_buy'}, 'strategy': 'trading_view'}, {'options': {'time': '1d', 'type': 'buy_or_strong_buy'}, 'strategy': 'trading_view'}] # list your strategy here
bot_martingale_volume_coefficient = '1.05'
bot_martingale_step_coefficient = '1.0'
bot_safety_order_step_percentage = '2'
bot_take_profit_type = 'total'

######################
# risk configuration #
######################
# How much of your bankroll do you want to use
risk_percentage = 325

###################################
# init python wrapper for 3commas #
###################################
p3cw = Py3CW(
key=threecommas_key,
secret=threecommas_secret,
request_options={
'request_timeout': 10,
'nr_of_retries': 1,
'retry_status_codes': [502]
}
)

######################
# auto configuration #
######################

error, data_accounts = p3cw.request(
entity='accounts',
action=''
)

for datapoint_account in data_accounts:
if datapoint_account['name'] == account_name:
account_balance = round(float(datapoint_account['usd_amount']),2)
account_id = str(datapoint_account['id']) # Account ID of your Exchange in 3commas
# print("Account Balance ($):", account_balance)

if account_balance > 0:
pass
else:
exit("Bad account information, balance = 0")

p3cw.request
error, data_bots = p3cw.request(
entity='bots',
action=''
)

######################
# auto calculation #
######################
print("----------START-----------")
base_order_size = int(bot_safety_order_size / bot_safety_order_size)
adj_safety_order_size = bot_safety_order_size
total_final_required_balance_array = []
total_final_required_balance = 0

# Calculate Safety Order Maximum Volume
for adj_safety_order_size in range(bot_safety_order_size, bot_safety_order_size + 100000):
step_value = float(0)
total_value = float(0)
step_value = step_value + adj_safety_order_size

total_value = round(float(base_order_size), 2)
x = 0
for x in range(1, bot_max_safety_orders_count + 1):
base_order_size = int(adj_safety_order_size / 2)
if x == 1:
step_value = round(step_value, 2)
else:
step_value = round((step_value * 1.05), 2)
total_value = round(total_value + step_value, 2)
# print("#", x, " - ", float(step_value), " - ", float(total_value))
total_final_required_balance = round(total_value * int(bot_max_active_deals), 2)

if total_final_required_balance > (account_balance*(float(risk_percentage)/100)):
print("Recommended Safety Order ($):", adj_safety_order_size - 1)
break
else:
total_final_required_balance_array.append(total_final_required_balance)
total_final_required_balance = 0

total_final_required_balance = 0
adj_base_order_size = base_order_size
final_safety_order = adj_safety_order_size - 1

# Calculate Base Order Maximum Volume
for adj_base_order_size in range(base_order_size, base_order_size + 1000000):
step_value = float(0)
total_value = float(0)
step_value = step_value + final_safety_order
total_value = round(float(adj_base_order_size), 2)
x = 0
for x in range(1, bot_max_safety_orders_count + 1):
if x == 1:
step_value = round(step_value, 2)
else:
step_value = round((step_value * 1.05), 2)
total_value = round(total_value + step_value, 2)
total_final_required_balance = round(total_value * int(bot_max_active_deals), 2)

if (total_final_required_balance > (account_balance*(float(risk_percentage)/100))) or ((adj_base_order_size+1)>adj_safety_order_size/2):
print("Recommended Base Order ($):", adj_base_order_size - 1)
print("This configuration will use: $", total_final_required_balance_array[-1], ", which is $", round((account_balance*(risk_percentage/100))-total_final_required_balance_array[-1], 2), "below allowed maximum: $", round(account_balance + ((account_balance*(risk_percentage/100))-account_balance),2), "(account balance of $", account_balance, "+", risk_percentage, "% risk: $", round((account_balance*(risk_percentage/100))-account_balance,2), ")")
break
else:
total_final_required_balance_array.append(total_final_required_balance)
total_final_required_balance = 0

######################
# update 3commas bot #
######################

if 'yes' in test_run:
print("Test Run Completed!")
else:
error, update_bot = p3cw.request(
entity='bots',
action='update',
action_id=bot_id,
payload={
'account_id': account_id,
'bot_id': bot_id,
'name': bot_name,
'pairs': bot_pairs,
'base_order_volume': adj_base_order_size - 1, # this is auto calculated value that we're changing
'safety_order_volume': adj_safety_order_size - 1, # this is auto calculated value that we're changing
'take_profit': bot_take_profit,
'martingale_volume_coefficient': bot_martingale_volume_coefficient,
'martingale_step_coefficient': bot_martingale_step_coefficient,
'max_safety_orders': bot_max_safety_orders_count,
'active_safety_orders_count': bot_active_safety_orders_count,
'safety_order_step_percentage': bot_safety_order_step_percentage,
'take_profit_type': bot_take_profit_type,
'strategy_list': bot_strategy_list,
'max_active_deals': str(bot_max_active_deals),
'trailing_enabled': bot_trailing_enabled,
'trailing_deviation': bot_trailing_deviation
}
)
if error == {}:
print("Bot update completed!")
else:
print(error)
print("Bot update NOT completed!")

 

So, let’s test it just by running the code as a Python script.

First, we need to create a DCA bot in our paper trading account. Let’s name it Paper_DCA_Bot

Configure it like this:

As you can see, this configuration uses

  • An account with the balance of 440,821.4283 USDT
  • We want to trade 10 USDT pairs: [‘USDT_1INCH’, ‘USDT_AAVE’, ‘USDT_ADA’,’USDT_AGLD’, ‘USDT_ALGO’, ‘USDT_ATOM’,’USDT_BTC’, ‘USDT_DOGE’, ‘USDT_LTC’,’USDT_XRP’]
  • Taking profit in Quote (USDT) currency
  • Max safety trades count: 10
  • Max active safety trades count: 1
  • Price deviation to open safety orders (% from initial order): 1%
  • Safety order volume scale: 1
  • Safety order step scale: 1

3Commas is telling us, that this setting would use a maximum of 2,647 USDT of our total balance of 440821.4283 USDT (or 0.6%).

Now, let’s run our script to calculate the base and safety order sizes for us to actually leverage the size of our entire USDT account.

First, we need to configure the script to replicate the above settings in code:

And here is the result:

Now, let’s log into the 3commas bot. As we can see, the values were all automatically adjusted for us.

Voila!, it worked:

The base order and safety order calculations look correct:

Order № Deviation, % Order Price Average price Required price Required change, % Total
Size (1INCH) Volume (USDT) Size (1INCH) Volume (USDT)
BO 0 355.5 1684.359 4.738 4.738 4.818 1.68847615 355.5 1684.359
1 1 718.6 3370.234 4.69 4.70588678 4.785 2.02558635 1074.1 5054.593
2 2 762.2 3538.8946 4.643 4.67978413 4.759 2.49838466 1836.3 8593.4876
3 3 808.6 3715.517 4.595 4.65386388 4.732 2.98150163 2644.9 12309.0046
4 4 857.8 3901.2744 4.548 4.62793816 4.706 3.47405452 3502.7 16210.279
5 5 910.1 4096.3601 4.501 4.60175831 4.679 3.95467673 4412.8 20306.6391
6 6 965.9 4301.1527 4.453 4.57504449 4.652 4.46889737 5378.7 24607.7918
7 7 1025 4516.15 4.406 4.5479866 4.625 4.97049477 6403.7 29123.9418
8 8 1088.1 4741.93980001 4.358 4.52039317 4.597 5.48416704 7491.8 33865.88160001
9 9 1155 4979.20500001 4.311 4.49242339 4.568 5.96149385 8646.8 38845.08660002
10 10 1226.1 5228.0904 4.264 4.46405584 4.539 6.44934333 9872.9 44073.17700002

 

Increasing Risk

How about if we wanted to run at 150% risk?

I’ve coded the script to accommodate this. Just change the script to run at:

risk_percentage = 150

Run it again, and the newly adjusted values come through, telling us that instead of using 440k USDT, we will risk across all trading pairs 150% of our total: 661k USDT:

The base order and safety order calculations look correct:

Order № Deviation, % Order Price Average price Required price Required change, % Total
Size (1INCH) Volume (USDT) Size (1INCH) Volume (USDT)
BO 0 536.9 2527.1883 4.707 4.707 4.787 1.69959634 536.9 2527.1883
1 1 1085.3 5056.41270001 4.659 4.67488657 4.754 2.03906417 1622.2 7583.60100001
2 2 1151.1 5308.87320001 4.612 4.64878455 4.727 2.49349522 2773.3 12892.47420002
3 3 1221.1 5574.3215 4.565 4.62317136 4.701 2.97918948 3994.4 18466.79570002
4 4 1295.5 5853.069 4.518 4.59741482 4.675 3.47498893 5289.9 24319.86470002
5 5 1374.6 6145.83660001 4.471 4.57134088 4.649 3.98121225 6664.5 30465.70130003
6 6 1458.7 6453.2888 4.424 4.54488257 4.622 4.4755877 8123.2 36918.99010003
7 7 1548 6775.596 4.377 4.5180108 4.594 4.9577336 9671.2 43694.58610003
8 8 1643.1 7114.623 4.33 4.49070725 4.567 5.4734411 11314.3 50809.20910003
9 9 1744.2 7470.40860001 4.283 4.46296417 4.538 5.95377072 13058.5 58279.61770004
10 10 1851.7 7843.80120001 4.236 4.43477746 4.51 6.46836638 14910.2 66123.41890005

AWS Lambda

Now, all that is needed for the Python script to work automatically, is to schedule it on a Linux machine as a cronjob or Windows machine as a scheduled task and let it run. But considering the script is using 3commas read/write credentials, it’s probably not the best idea to store these credentials on your PC and also, whenever your workstation goes down, the script would also stop running. So, we can do better.

Let’s leverage Amazon Cloud and their serverless, event-driven compute service called AWS Lambda, which lets us run the Python code as a backend service without provisioning or managing servers and allows us to trigger Lambda as a cron job through EventBridge.

Login into our AWS console and first wire things for lambda to run.

Create the following:

  • VPC, unless you already have one
  • Private Lambda subnet
  • Public Lambda subnet
  • NAT Gateway that Lambda will use

You can follow this article to do the above: https://aws.amazon.com/premiumsupport/knowledge-center/internet-access-lambda-function/

Now, let’s create a Lambda script, that will adjust our board automatically:

  • Login to Lambda in AWS
  • Create a new function, call it: 3Commas_Auto_Compounding
  • Configure the VPC info:
  • Security groups need to be configured in such a way that only outbound access is allowed on port 443, meaning your script can get out, but nothing can get to it.
  • Configure the code config the way you want, also don’t forget to upload all dependencies as additional folders, for the Python script to run:
  • As you can see above, I do not expose credentials in the code, instead, I leverage the environment variables and store them there as key/value pairs:
  • Now we can create an AWS EventBridge cron trigger to run the script every 5 minutes:

 

And Voila! We’re done, the 3Commas base and safety order are recalculated and updated for us automatically every 5 minutes.

A simple cloud solution to a simple problem:

Enjoy :)