Report daily trading statistics for your algorithmic trading bot

As an Amazon Associate I earn from qualifying purchases.

5 Min Read

In the last post, I showed you how to Monitor your P/L for open trades. Today, let’s expand on this. I will be going through how to report daily trading statistics for your algorithmic trading bot such as:

  • Number of trades opened
  • Number of trades closed
  • Amount of profitable trades
  • Amount of losing trades
  • W/L Ratio
  • Account balance

I suggest sending these statistics to slack but like the last post, you can also use email if you wish to.

Getting your account information

Start by creating a new method and name it get_account_info with no arguments:

def get_account_info:

Next, fetch your account information using the MT5 API account_info method and assign it to a variable named res:

def get_account_info:
    res = mt5.account_info()

And finally return res:

def get_account_info:
    res = mt5.account_info()
    return res

Getting the trading history

When you are sending the daily stats notification, you will be fetching the last 24 hours of data. This means if you decide to send daily stats at 7 pm, everything from 7 pm yesterday until today will be included. Start by defining a new method named get_daily_trade_data which takes no arguments:

def get_daily_trade_data():

You may remember from Creating an algotrader/trading bot with Python – Part 1 – I mentioned your local time and broker time can be different. I highly suggest using your broker time when calculating the last 24 hours of data. Start by defining the current time adjusted for your broker time:

def get_daily_trade_data():
    now = datetime.now().astimezone(pytz.timezone('Europe/Athens'))
    now = datetime(now.year, now.month, now.day, hour=now.hour, minute=now.minute)

From the variable now, you need to subtract 24 hours using timedelta and assign this to a variable named yesterday:

def get_daily_trade_data():
    now = datetime.now().astimezone(pytz.timezone('Europe/Athens'))
    now = datetime(now.year, now.month, now.day, hour=now.hour, minute=now.minute)
    yesterday = now - timedelta(hours=24)

And call get_order_history passing in both variables and return the result res:

def get_daily_trade_data():
    now = datetime.now().astimezone(pytz.timezone('Europe/Athens'))
    now = datetime(now.year, now.month, now.day, hour=now.hour, minute=now.minute)
    yesterday = now - timedelta(hours=24)
    res = get_order_history(yesterday, now)
    return res

Formatting the stats into a message

Now that you have all the information you need, let’s go ahead and start formatting the data into a message that can be sent to slack. Define a new method and name it send_daily_stats. This method will also take no arguments:

def send_daily_stats():

Get the required account information using get_account_info() and assign it to a variable named account_info . Similarly, get the last 24 trade data by using get_daily_trade_data() and assign it to a variable named trades:

def send_daily_stats():
    account_info = get_account_info()
    trades = get_daily_trade_data()

Now, let’s start gathering all the information we care about. Start by getting balance from account_info:

def send_daily_stats():
    account_info = get_account_info()
    trades = get_daily_trade_data()

    balance = account_info.balance

Here is a reminder of what we want to send in the daily stats alert:

  • Number of trades opened
  • Number of trades closed
  • Amount of profitable trades
  • Amount of losing trades
  • W/L Ratio
  • Account balance

When we get the deal history, a trade will have 2 entries: one for opening the trade and one for closing. You can differentiate these entries by the type column. 0 is opened and 1 is closed. To get a count of opened trades and closed trades we will use the shape method to count the rows in each data frame. (More information on shape can be found here):

    # Calculate open trade count
    opened_trades = trades[trades['type'] == 0]
    open_trade_count = opened_trades.shape[0]

    # Calculate closed trade count
    closed_trades = trades[trades['type'] == 1]
    closed_trade_count = closed_trades.shape[0]

Calculate the amount of profitable and losing trades. For profitable trades, filter on the profit column for anything greater than zero. For losing, filter on profit less than zero. Get the counts by using the same shape method above:

    # Calculate profitable trades count
    profitable_trades = trades[trades['profit'] > 0]
    profitable_trades_count = profitable_trades.shape[0]

    # Calculate losing trades count
    losing_trades = trades[trades['profit'] < 0]
    losing_trades_count = losing_trades.shape[0]

The last thing you need to do is calculate the W/L Ratio. The W/L ratio is defined as

W/L Ratio = Amount of winning trades / Amount of losing trades

The code in our case will look like the following:

winLossRatio = profitable_trades_count / losing_trades_count

Great! Now we have all the information we need to send our stats alert. In the next section, I take you through how to format this information into a message for slack.

Formatting the message for slack

We will be using JSON to send a formatted message to slack. Here is a preview of what our slack message template will look like:

Start by copying the following JSON template and assign it to a variable named slack_json in the send_daily_stats method. (This can also be loaded in as a JSON file using json.loads):

slack_json = [
		{
			"type": "section",
			"text": {
				"type": "mrkdwn",
				"text": "Daily report"
			}
		},
		{
			"type": "divider"
		},
		{
			"type": "section",
			"text": {
				"type": "mrkdwn",
				"text": "*Account Balance:* "
			}
		},
		{
			"type": "section",
			"text": {
				"type": "mrkdwn",
				"text": "*Trades opened:* "
			}
		},
		{
			"type": "section",
			"text": {
				"type": "mrkdwn",
				"text": "*Trades closed:* "
			}
		},
		{
			"type": "section",
			"text": {
				"type": "mrkdwn",
				"text": "*Profitable trades:* "
			}
		},
		{
			"type": "section",
			"text": {
				"type": "mrkdwn",
				"text": "*Losing trades:* "
			}
		},
		{
			"type": "section",
			"text": {
				"type": "mrkdwn",
				"text": "*W/L Ratio:* "
			}
		}
	]

Modify the text field in each entry to include the variables created in the previous section. These variables should be wrapped in the str method.

E.g "text": "Account Balance: " becomes "text": "Account Balance: " + str(balance)

"text": "*Trades opened:*" becomes "text": "*Trades opened:* " + str(open_trade_count)

etc.

After this has been done, your are ready to send your slack message!

    send_notification("", slack_json)

Call send_notification with 2 arguments, leaving the first argument as an empty string and the second as slack_json:

send_notification("", slack_json)

Okay, so you may have noticed the send_notification method only takes one argument. We need to add a second optional argument blocks to send the message as JSON.

Find send_notification and modify the method signature by adding blocks = None at the end as an optional argument:

def send_notification(message, blocks = None):

Now, specify if blocks is None then call client.chat_postMessage without the blocks argument, otherwise call it with this argument:

def send_notification(message, blocks = None):
    if(blocks is None):
        client.chat_postMessage(channel='#algotrader', text=message)
    else:
        client.chat_postMessage(channel='#algotrader', text=message, blocks=blocks)

Sending daily stats on a schedule

The final part is to add daily stats to the schedule. Find the live_trading method and add the following line to the end of all the schedule calls:

    schedule.every().day.at("19:00").do(send_daily_stats)

Your live_trading method should now look like the following:

def live_trading(strategy):
    schedule.every().hour.at(":00").do(run_trader, mt5.TIMEFRAME_M15, strategy)
    schedule.every().hour.at(":15").do(run_trader, mt5.TIMEFRAME_M15, strategy)
    schedule.every().hour.at(":30").do(run_trader, mt5.TIMEFRAME_M15, strategy)
    schedule.every().hour.at(":45").do(run_trader, mt5.TIMEFRAME_M15, strategy)
    schedule.every(4).hours.at(":00").do(send_stats)
    schedule.every().day.at("19:00").do(send_daily_stats)

    while True:
        schedule.run_pending()
        time.sleep(1)

Testing the code

To test the code, I will manually trigger the send_daily_stats method. I expected to see my message in slack formatted as specified above:

As you can see, my message has been posted to my slack channel and includes all the daily stats relevant to my account.

If you are interested in learning more about algo trading and trading systems, I highly recommend reading this book. I have taken some of my own trading ideas and strategies from this book. It also provided me a great insight into effective back testing. Check it out here.

That’s all for how to report daily trading statistics for your Algorithmic trading bot! Check back on Friday to see how to turn your strategy off if you have reached your maximum account drawdown. As always, if you have any questions or comments please feel free to post them below. Additionally, if you run into any issues, please let me know.

4 thoughts on “Report daily trading statistics for your algorithmic trading bot”

  1. Avatar

    Hi Connor,
    I’m having some issues trying to deal with this error when I run the code:

    Traceback (most recent call last):
    File “C:\Python\Python39\lib\site-packages\pandas\core\indexes\base.py”, line 3080, in get_loc
    return self._engine.get_loc(casted_key)
    File “pandas\_libs\index.pyx”, line 70, in pandas._libs.index.IndexEngine.get_loc
    File “pandas\_libs\index.pyx”, line 101, in pandas._libs.index.IndexEngine.get_loc
    File “pandas\_libs\hashtable_class_helper.pxi”, line 4554, in pandas._libs.hashtable.PyObjectHashTable.get_item
    File “pandas\_libs\hashtable_class_helper.pxi”, line 4562, in pandas._libs.hashtable.PyObjectHashTable.get_item
    KeyError: ‘type’

    The above exception was the direct cause of the following exception:

    Traceback (most recent call last):
    File “C:\Python\Python39\Scripts\trader.py”, line 368, in
    live_trading(current_strategy)
    File “C:\Python\Python39\Scripts\trader.py”, line 358, in live_trading
    schedule.run_pending()
    File “C:\Python\Python39\lib\site-packages\schedule\__init__.py”, line 592, in run_pending
    default_scheduler.run_pending()
    File “C:\Python\Python39\lib\site-packages\schedule\__init__.py”, line 94, in run_pending
    self._run_job(job)
    File “C:\Python\Python39\lib\site-packages\schedule\__init__.py”, line 147, in _run_job
    ret = job.run()
    File “C:\Python\Python39\lib\site-packages\schedule\__init__.py”, line 491, in run
    ret = self.job_func()
    File “C:\Python\Python39\Scripts\trader.py”, line 346, in run_trader
    send_daily_stats()
    File “C:\Python\Python39\Scripts\trader.py”, line 235, in send_daily_stats
    opened_trades = trades[trades[‘type’] == 0]
    File “C:\Python\Python39\lib\site-packages\pandas\core\frame.py”, line 3024, in __getitem__
    indexer = self.columns.get_loc(key)
    File “C:\Python\Python39\lib\site-packages\pandas\core\indexes\base.py”, line 3082, in get_loc
    raise KeyError(key) from err
    KeyError: ‘type’

    It keeps complaining of KeyError ‘type’ in:

    def send_daily_stats():
    account_info = get_account_info()
    trades = get_daily_trade_data()

    balance = account_info.balance

    # Calculate open trade count
    opened_trades = trades[trades[‘type’] == 0]
    open_trade_count = opened_trades.shape[0]

    # Calculate closed trade count
    closed_trades = trades[trades[‘type’] == 1]
    closed_trade_count = closed_trades.shape[0]

    # Calculate profitable trades count
    profitable_trades = trades[trades[‘profit’] > 0]
    profitable_trades_count = profitable_trades.shape[0]

    # Calculate losing trades count
    losing_trades = trades[trades[‘profit’] < 0]
    losing_trades_count = losing_trades.shape[0]

    winLossRatio = profitable_trades_count / losing_trades_count

    slack_json = [
    {
    "type": "section",
    "text": {
    "type": "mrkdwn",
    "text": "Daily report"
    }
    },
    {
    "type": "divider"
    },
    {
    "type": "section",
    "text": {
    "type": "mrkdwn",
    "text": "Account Balance: " + str(balance)
    }
    },
    {
    "type": "section",
    "text": {
    "type": "mrkdwn",
    "text": "*Trades opened:* " + str(open_trade_count)
    }
    },
    {
    "type": "section",
    "text": {
    "type": "mrkdwn",
    "text": "*Trades closed:* " + str(closed_trade_count)
    }
    },
    {
    "type": "section",
    "text": {
    "type": "mrkdwn",
    "text": "*Profitable trades:* " + str(profitable_trades_count)
    }
    },
    {
    "type": "section",
    "text": {
    "type": "mrkdwn",
    "text": "*Losing trades:* " + str(losing_trades_count)
    }
    },
    {
    "type": "section",
    "text": {
    "type": "mrkdwn",
    "text": "*W/L Ratio:* " + str(winLossRatio)
    }
    }
    ]

    send_notification("", slack_json)

    Not sure what I did wrong.

    1. Avatar

      I’m stuck on this too but I don’t know if they even reply or look at these messages at this point…you message or DM them here directly or on Reddit and you just get ignored…

      1. Conor O'Hanlon

        This code counts on the fact that a trade has been opened. I might need to add a None check for the trades variable! Apologies on not getting back soon, I’ve been extremely busy the last few weeks 🙂

    2. Conor O'Hanlon

      This code counts on the fact that a trade has been opened. I might need to add a None check for the trades variable! Apologies on not getting back soon, I’ve been extremely busy the last few weeks

Leave a Comment

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

Subscribe to my newsletter to keep up to date with my latest posts

Holler Box