com.gazbert.bxbot.core.engine.TradingEngine.java Source code

Java tutorial

Introduction

Here is the source code for com.gazbert.bxbot.core.engine.TradingEngine.java

Source

/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2015 Gareth Jon Lynch
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

package com.gazbert.bxbot.core.engine;

import com.gazbert.bxbot.core.mail.EmailAlerter;
import com.gazbert.bxbot.core.util.ConfigurableComponentFactory;
import com.gazbert.bxbot.domain.engine.EngineConfig;
import com.gazbert.bxbot.domain.exchange.AuthenticationConfig;
import com.gazbert.bxbot.domain.exchange.ExchangeConfig;
import com.gazbert.bxbot.domain.exchange.NetworkConfig;
import com.gazbert.bxbot.domain.exchange.OtherConfig;
import com.gazbert.bxbot.domain.market.MarketConfig;
import com.gazbert.bxbot.domain.strategy.StrategyConfig;
import com.gazbert.bxbot.exchange.api.ExchangeAdapter;
import com.gazbert.bxbot.exchange.api.impl.AuthenticationConfigImpl;
import com.gazbert.bxbot.exchange.api.impl.ExchangeConfigImpl;
import com.gazbert.bxbot.exchange.api.impl.NetworkConfigImpl;
import com.gazbert.bxbot.exchange.api.impl.OtherConfigImpl;
import com.gazbert.bxbot.repository.EngineConfigRepository;
import com.gazbert.bxbot.repository.ExchangeConfigRepository;
import com.gazbert.bxbot.repository.MarketConfigRepository;
import com.gazbert.bxbot.repository.StrategyConfigRepository;
import com.gazbert.bxbot.strategy.api.StrategyException;
import com.gazbert.bxbot.strategy.api.TradingStrategy;
import com.gazbert.bxbot.strategy.api.impl.StrategyConfigItems;
import com.gazbert.bxbot.trading.api.BalanceInfo;
import com.gazbert.bxbot.trading.api.ExchangeNetworkException;
import com.gazbert.bxbot.trading.api.Market;
import com.gazbert.bxbot.trading.api.TradingApiException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.util.*;

/**
 * The main Trading Engine.
 * <p>
 * The engine has been coded to fail *hard and fast* whenever something unexpected happens. If Email
 * Alerts are enabled, a message will be sent with details of the problem before the bot is shutdown.
 * <p>
 * The only time the bot does not fail hard and fast is for network issues connecting to the exchange - it logs the error
 * and retries at next trade cycle.
 * <p>
 * To keep things simple:
 * - The engine is single threaded.
 * - The engine only supports trading on 1 exchange per instance of the bot, i.e. 1 Exchange Adapter per process.
 * - The engine only supports 1 Trading Strategy per Market.
 *
 * @author gazbert
 */
@Component
@ComponentScan(basePackages = { "com.gazbert.bxbot" })
public class TradingEngine {

    private static final Logger LOG = LogManager.getLogger();

    // Email Alert error message stuff
    private static final String CRITICAL_EMAIL_ALERT_SUBJECT = "CRITICAL Alert message from BX-bot";
    private static final String DETAILS_ERROR_MSG_LABEL = " Details: ";
    private static final String CAUSE_ERROR_MSG_LABEL = " Cause: ";
    private static final String NEWLINE = System.getProperty("line.separator");
    private static final String HORIZONTAL_RULE = "--------------------------------------------------" + NEWLINE;

    /*
     * Trade execution interval in secs. The time we wait/sleep in between trade cycles.
     */
    private static int tradeExecutionInterval;

    /*
     * Control flag decides if the Trading Engine lives or dies.
     */
    private volatile boolean keepAlive = true;

    /*
     * Is Trading Engine already running? Used to prevent multiple 'starts' of the engine.
     */
    private boolean isRunning = false;

    /*
     * Monitor to use when checking if Trading Engine is running.
     */
    private static final Object IS_RUNNING_MONITOR = new Object();

    /*
     * The thread the Trading Engine is running in.
     */
    private Thread engineThread;

    /*
     * Map of Trading Strategy descriptions from config.
     */
    private final Map<String, StrategyConfig> strategyDescriptions = new HashMap<>();

    /*
     * List of cached Trading Strategy implementations for the Trade Engine to execute.
     */
    private final List<TradingStrategy> tradingStrategiesToExecute = new ArrayList<>();

    /*
     * The emergency stop currency value is used to prevent a catastrophic loss on the exchange.
     * It is set to the currency short code, e.g. BTC, USD.
     * This is normally the currency you intend to hold a long position in.
     */
    private String emergencyStopCurrency;

    /*
     * The Emergency Stop balance.
     * It is used to prevent a catastrophic loss on the exchange.
     * The Trading Engine checks this value at the start of every trade cycle: if the balance on
     * the exchange drops below this value, the Trading Engine will stop trading on all markets.
     * Manual intervention is then required to restart the bot.
     */
    private BigDecimal emergencyStopBalance;

    private EmailAlerter emailAlerter;
    private ExchangeAdapter exchangeAdapter;

    // Repos
    private final ExchangeConfigRepository exchangeConfigRepository;
    private final EngineConfigRepository engineConfigRepository;
    private final StrategyConfigRepository strategyConfigRepository;
    private final MarketConfigRepository marketConfigRepository;

    @Autowired
    public TradingEngine(ExchangeConfigRepository exchangeConfigRepository,
            EngineConfigRepository engineConfigRepository, StrategyConfigRepository strategyConfigRepository,
            MarketConfigRepository marketConfigRepository, EmailAlerter emailAlerter) {

        LOG.info(() -> "Initialising Trading Engine...");

        Assert.notNull(exchangeConfigRepository, "exchangeConfigRepository dependency cannot be null!");
        this.exchangeConfigRepository = exchangeConfigRepository;

        Assert.notNull(engineConfigRepository, "engineConfigRepository dependency cannot be null!");
        this.engineConfigRepository = engineConfigRepository;

        Assert.notNull(strategyConfigRepository, "strategyConfigRepository dependency cannot be null!");
        this.strategyConfigRepository = strategyConfigRepository;

        Assert.notNull(marketConfigRepository, "marketConfigRepository dependency cannot be null!");
        this.marketConfigRepository = marketConfigRepository;

        Assert.notNull(emailAlerter, "emailAlerter dependency cannot be null!");
        this.emailAlerter = emailAlerter;
    }

    public void start() throws IllegalStateException {

        synchronized (IS_RUNNING_MONITOR) {
            if (isRunning) {
                final String errorMsg = "Cannot start Trading Engine because it is already running!";
                LOG.error(errorMsg);
                throw new IllegalStateException(errorMsg);
            }

            // first time to start
            isRunning = true;
        }

        // store this so we can shutdown the engine later
        engineThread = Thread.currentThread();

        initConfig();
        runMainControlLoop();
    }

    private void initConfig() {

        LOG.info(() -> "Initialising BX-Bot config...");

        // the sequence order of these methods is significant - don't change it.
        loadExchangeAdapterConfig();
        loadEngineConfig();
        loadTradingStrategyConfig();
        loadMarketConfigAndInitialiseTradingStrategies();
    }

    /*
     * The main control loop.
     * We loop infinitely unless an unexpected exception occurs.
     * The code fails hard and fast if an unexpected occurs. Network exceptions *should* recover.
     */
    private void runMainControlLoop() {

        LOG.info(() -> "Starting Trading Engine...");

        while (keepAlive) {

            try {

                LOG.info(() -> "*** Starting next trade cycle... ***");

                // Emergency Stop Check MUST run at start of every trade cycle.
                if (isEmergencyStopLimitBreached()) {
                    break;
                }

                // Execute the Trading Strategies
                for (final TradingStrategy tradingStrategy : tradingStrategiesToExecute) {
                    LOG.info(() -> "Executing Trading Strategy ---> " + tradingStrategy.getClass().getSimpleName());
                    tradingStrategy.execute();
                }

                LOG.info(() -> "*** Sleeping " + tradeExecutionInterval + "s til next trade cycle... ***");

                try {
                    Thread.sleep(tradeExecutionInterval * 1000);
                } catch (InterruptedException e) {
                    LOG.warn("Control Loop thread interrupted when sleeping before next trade cycle");
                    Thread.currentThread().interrupt();
                }

            } catch (ExchangeNetworkException e) {

                /*
                 * We have a network connection issue reported by Exchange Adapter when called directly from
                 * Trading Engine. Current policy is to log it and sleep until next trade cycle.
                 */
                final String WARNING_MSG = "A network error has occurred in Exchange Adapter! "
                        + "BX-bot will attempt next trade in " + tradeExecutionInterval + "s...";
                LOG.error(WARNING_MSG, e);

                try {
                    Thread.sleep(tradeExecutionInterval * 1000);
                } catch (InterruptedException e1) {
                    LOG.warn("Control Loop thread interrupted when sleeping before next trade cycle");
                    Thread.currentThread().interrupt();
                }

            } catch (TradingApiException e) {

                /*
                 * A serious issue has occurred in the Exchange Adapter.
                 * Current policy is to log it, send email alert if required, and shutdown bot.
                 */
                final String FATAL_ERROR_MSG = "A FATAL error has occurred in Exchange Adapter!";
                LOG.fatal(FATAL_ERROR_MSG, e);
                emailAlerter.sendMessage(CRITICAL_EMAIL_ALERT_SUBJECT,
                        buildCriticalEmailAlertMsgContent(FATAL_ERROR_MSG + DETAILS_ERROR_MSG_LABEL + e.getMessage()
                                + CAUSE_ERROR_MSG_LABEL + e.getCause(), e));
                keepAlive = false;

            } catch (StrategyException e) {

                /*
                 * A serious issue has occurred in the Trading Strategy.
                 * Current policy is to log it, send email alert if required, and shutdown bot.
                 */
                final String FATAL_ERROR_MSG = "A FATAL error has occurred in Trading Strategy!";
                LOG.fatal(FATAL_ERROR_MSG, e);
                emailAlerter.sendMessage(CRITICAL_EMAIL_ALERT_SUBJECT,
                        buildCriticalEmailAlertMsgContent(FATAL_ERROR_MSG + DETAILS_ERROR_MSG_LABEL + e.getMessage()
                                + CAUSE_ERROR_MSG_LABEL + e.getCause(), e));
                keepAlive = false;

            } catch (Exception e) {

                /*
                 * A serious and *unexpected* issue has occurred in the Exchange Adapter or Trading Strategy.
                 * Current policy is to log it, send email alert if required, and shutdown bot.
                 */
                final String FATAL_ERROR_MSG = "An unexpected FATAL error has occurred in Exchange Adapter or Trading Strategy!";
                LOG.fatal(FATAL_ERROR_MSG, e);
                emailAlerter.sendMessage(CRITICAL_EMAIL_ALERT_SUBJECT,
                        buildCriticalEmailAlertMsgContent(FATAL_ERROR_MSG + DETAILS_ERROR_MSG_LABEL + e.getMessage()
                                + CAUSE_ERROR_MSG_LABEL + e.getCause(), e));
                keepAlive = false;
            }
        }

        LOG.fatal("BX-bot is shutting down NOW!");
        synchronized (IS_RUNNING_MONITOR) {
            isRunning = false;
        }
    }

    /*
     * Shutdown the Trading Engine.
     * Might be called from a different thread.
     * TODO currently not used - but will eventually be called from Admin Console.
     */
    public void shutdown() {

        LOG.info(() -> "Shutdown request received!");
        LOG.info(() -> "Engine originally started in thread: " + engineThread);

        keepAlive = false;
        engineThread.interrupt(); // poke it in case bot is sleeping
    }

    synchronized boolean isRunning() {
        LOG.info(() -> "isRunning: " + isRunning);
        return isRunning;
    }

    /*
     * Checks if the Emergency Stop Currency (e.g. USD, BTC) wallet balance on exchange has gone *below* configured limit.
     * If the balance cannot be obtained or has dropped below the configured limit, we notify the main control loop to
     * immediately shutdown the bot.
     *
     * This check is here to help protect runaway losses due to:
     * - 'buggy' Trading Strategies
     * - Unforeseen bugs in the Trading Engine and Exchange Adapter
     * - the exchange sending corrupt order book data and the Trading Strategy being misled... this has happened.
     */
    private boolean isEmergencyStopLimitBreached() throws TradingApiException, ExchangeNetworkException {

        boolean isEmergencyStopLimitBreached = true;

        LOG.info(() -> "Performing Emergency Stop check...");

        BalanceInfo balanceInfo;
        try {
            balanceInfo = exchangeAdapter.getBalanceInfo();
        } catch (TradingApiException e) {
            final String errorMsg = "Failed to get Balance info from exchange to perform Emergency Stop check - letting"
                    + " Trade Engine error policy decide what to do next...";
            LOG.error(errorMsg, e);
            // re-throw to main loop - might only be connection issue and it will retry...
            throw e;
        }

        final Map<String, BigDecimal> balancesAvailable = balanceInfo.getBalancesAvailable();
        final BigDecimal currentBalance = balancesAvailable.get(emergencyStopCurrency);
        if (currentBalance == null) {
            final String errorMsg = "Emergency stop check: Failed to get current Emergency Stop Currency balance as '"
                    + emergencyStopCurrency + "' key into Balances map " + "returned null. Balances returned: "
                    + balancesAvailable;
            LOG.error(errorMsg);
            throw new IllegalStateException(errorMsg);
        } else {

            LOG.info(() -> "Emergency Stop Currency balance available on exchange is ["
                    + new DecimalFormat("#.########").format(currentBalance) + "] " + emergencyStopCurrency);

            LOG.info(() -> "Balance that will stop ALL trading across ALL markets is ["
                    + new DecimalFormat("#.########").format(emergencyStopBalance) + "] " + emergencyStopCurrency);

            if (currentBalance.compareTo(emergencyStopBalance) < 0) {
                final String balanceBlownErrorMsg = "EMERGENCY STOP triggered! - Current Emergency Stop Currency ["
                        + emergencyStopCurrency + "] wallet balance ["
                        + new DecimalFormat("#.########").format(currentBalance) + "] on exchange "
                        + "is lower than configured Emergency Stop balance ["
                        + new DecimalFormat("#.########").format(emergencyStopBalance) + "] "
                        + emergencyStopCurrency;

                LOG.fatal(balanceBlownErrorMsg);
                emailAlerter.sendMessage(CRITICAL_EMAIL_ALERT_SUBJECT,
                        buildCriticalEmailAlertMsgContent(balanceBlownErrorMsg, null));
            } else {

                isEmergencyStopLimitBreached = false;
                LOG.info(() -> "Emergency Stop check PASSED!");
            }
        }
        return isEmergencyStopLimitBreached;
    }

    private String buildCriticalEmailAlertMsgContent(String errorDetails, Throwable exception) {

        final StringBuilder msgContent = new StringBuilder("A CRITICAL error event has occurred on BX-bot.");
        msgContent.append(NEWLINE).append(NEWLINE);

        msgContent.append(HORIZONTAL_RULE);
        msgContent.append("Exchange Adapter:");
        msgContent.append(NEWLINE).append(NEWLINE);
        msgContent.append(exchangeAdapter.getClass().getName());
        msgContent.append(NEWLINE).append(NEWLINE);

        msgContent.append(HORIZONTAL_RULE);
        msgContent.append("Event Time:");
        msgContent.append(NEWLINE).append(NEWLINE);
        msgContent.append(new Date());
        msgContent.append(NEWLINE).append(NEWLINE);

        msgContent.append(HORIZONTAL_RULE);
        msgContent.append("Event Details:");
        msgContent.append(NEWLINE).append(NEWLINE);
        msgContent.append(errorDetails);
        msgContent.append(NEWLINE).append(NEWLINE);

        msgContent.append(HORIZONTAL_RULE);
        msgContent.append("Action Taken:");
        msgContent.append(NEWLINE).append(NEWLINE);
        msgContent.append("The bot will shutdown NOW! Check the bot logs for more information.");
        msgContent.append(NEWLINE).append(NEWLINE);

        if (exception != null) {
            msgContent.append(HORIZONTAL_RULE);
            msgContent.append("Stacktrace:");
            msgContent.append(NEWLINE).append(NEWLINE);
            final StringWriter stringWriter = new StringWriter();
            final PrintWriter printWriter = new PrintWriter(stringWriter);
            exception.printStackTrace(printWriter);
            msgContent.append(stringWriter.toString());
        }

        return msgContent.toString();
    }

    // ------------------------------------------------------------------------
    // Config loading methods
    // ------------------------------------------------------------------------

    private void loadExchangeAdapterConfig() {

        final ExchangeConfig domainExchangeConfig = exchangeConfigRepository.getConfig();
        LOG.info(() -> "Fetched Exchange config from repository: " + domainExchangeConfig);

        exchangeAdapter = ConfigurableComponentFactory.createComponent(domainExchangeConfig.getExchangeAdapter());
        LOG.info(() -> "Trading Engine will use Exchange Adapter for: " + exchangeAdapter.getImplName());

        final ExchangeConfigImpl adapterExchangeConfig = new ExchangeConfigImpl();

        // Fetch optional network config
        final NetworkConfig networkConfig = domainExchangeConfig.getNetworkConfig();
        if (networkConfig != null) {

            final NetworkConfigImpl adapterNetworkConfig = new NetworkConfigImpl();
            adapterNetworkConfig.setConnectionTimeout(networkConfig.getConnectionTimeout());

            // Grab optional non-fatal error codes
            final List<Integer> nonFatalErrorCodes = networkConfig.getNonFatalErrorCodes();
            if (nonFatalErrorCodes != null) {
                adapterNetworkConfig.setNonFatalErrorCodes(nonFatalErrorCodes);
            } else {
                LOG.info(
                        () -> "No (optional) NetworkConfiguration NonFatalErrorCodes have been set for Exchange Adapter: "
                                + exchangeAdapter.getImplName());
            }

            // Grab optional non-fatal error messages
            final List<String> nonFatalErrorMessages = networkConfig.getNonFatalErrorMessages();
            if (nonFatalErrorMessages != null) {
                adapterNetworkConfig.setNonFatalErrorMessages(nonFatalErrorMessages);
            } else {
                LOG.info(
                        () -> "No (optional) NetworkConfiguration NonFatalErrorMessages have been set for Exchange Adapter: "
                                + exchangeAdapter.getImplName());
            }

            adapterExchangeConfig.setNetworkConfig(adapterNetworkConfig);
            LOG.info(() -> "NetworkConfiguration has been set: " + adapterNetworkConfig);

        } else {
            LOG.info(() -> "No (optional) NetworkConfiguration has been set for Exchange Adapter: "
                    + exchangeAdapter.getImplName());
        }

        // Fetch optional authentication config
        final AuthenticationConfig authenticationConfig = domainExchangeConfig.getAuthenticationConfig();
        if (authenticationConfig != null) {

            final AuthenticationConfigImpl adapterAuthenticationConfig = new AuthenticationConfigImpl();
            adapterAuthenticationConfig.setItems(authenticationConfig.getItems());
            adapterExchangeConfig.setAuthenticationConfig(adapterAuthenticationConfig);

            // WARNING - careful when you log this
            //            LOG.info(() ->
            //                    "AuthenticationConfiguration has been set: " + adapterAuthenticationConfig);

        } else {
            LOG.info(() -> "No (optional) AuthenticationConfiguration has been set for Exchange Adapter: "
                    + exchangeAdapter.getImplName());
        }

        // Fetch optional 'other' config
        final OtherConfig otherConfig = domainExchangeConfig.getOtherConfig();
        if (otherConfig != null) {

            final OtherConfigImpl adapterOtherConfig = new OtherConfigImpl();
            adapterOtherConfig.setItems(otherConfig.getItems());
            adapterExchangeConfig.setOtherConfig(adapterOtherConfig);
            LOG.info(() -> "OtherConfiguration has been set: " + adapterOtherConfig);

        } else {
            LOG.info(() -> "No (optional) OtherConfiguration has been set for Exchange Adapter: "
                    + exchangeAdapter.getImplName());
        }

        exchangeAdapter.init(adapterExchangeConfig);
    }

    private void loadEngineConfig() {

        final EngineConfig engineConfig = engineConfigRepository.getConfig();
        LOG.info(() -> "Fetched Engine config from repository: " + engineConfig);

        tradeExecutionInterval = engineConfig.getTradeCycleInterval();
        emergencyStopCurrency = engineConfig.getEmergencyStopCurrency();
        emergencyStopBalance = engineConfig.getEmergencyStopBalance();
    }

    private void loadTradingStrategyConfig() {

        final List<StrategyConfig> strategies = strategyConfigRepository.findAllStrategies();
        LOG.debug(() -> "Fetched Strategy config from repository: " + strategies);

        for (final StrategyConfig strategy : strategies) {
            strategyDescriptions.put(strategy.getId(), strategy);
            LOG.info(() -> "Registered Trading Strategy with Trading Engine - ID: " + strategy.getId());
        }
    }

    private void loadMarketConfigAndInitialiseTradingStrategies() {

        final List<MarketConfig> markets = marketConfigRepository.findAllMarkets();
        LOG.info(() -> "Fetched Markets config from repository: " + markets);

        // used only as crude mechanism for checking for duplicate Markets
        final Set<Market> loadedMarkets = new HashSet<>();

        // Load em up and create the Strategies
        for (final MarketConfig market : markets) {

            final String marketName = market.getLabel();
            if (!market.isEnabled()) {
                LOG.info(() -> marketName + " market is NOT enabled for trading - skipping to next market...");
                continue;
            }

            final Market tradingMarket = new Market(marketName, market.getId(), market.getBaseCurrency(),
                    market.getCounterCurrency());
            final boolean wasAdded = loadedMarkets.add(tradingMarket);
            if (!wasAdded) {
                final String errorMsg = "Found duplicate Market! Market details: " + market;
                LOG.fatal(errorMsg);
                throw new IllegalArgumentException(errorMsg);
            }

            // Get the strategy to use for this Market
            final String strategyToUse = market.getTradingStrategy();
            LOG.info(() -> "Market Trading Strategy Id: " + strategyToUse);

            if (strategyDescriptions.containsKey(strategyToUse)) {
                final StrategyConfig tradingStrategy = strategyDescriptions.get(strategyToUse);
                final String tradingStrategyClassname = tradingStrategy.getClassName();

                // Grab optional config for the Trading Strategy
                final StrategyConfigItems tradingStrategyConfig = new StrategyConfigItems();
                final Map<String, String> configItems = tradingStrategy.getConfigItems();
                if (configItems != null) {
                    tradingStrategyConfig.setItems(configItems);
                } else {
                    LOG.info(() -> "No (optional) configuration has been set for Trading Strategy: "
                            + strategyToUse);
                }

                LOG.info(() -> "StrategyConfigImpl (optional): " + tradingStrategyConfig);

                /*
                 * Load the Trading Strategy impl, instantiate it, set its config, and store in the cached
                 * Trading Strategy execution list.
                 */
                final TradingStrategy strategyImpl = ConfigurableComponentFactory
                        .createComponent(tradingStrategyClassname);
                strategyImpl.init(exchangeAdapter, tradingMarket, tradingStrategyConfig);

                LOG.info(() -> "Initialized trading strategy successfully. Name: [" + tradingStrategy.getLabel()
                        + "] Class: " + tradingStrategy.getClassName());

                tradingStrategiesToExecute.add(strategyImpl);
            } else {

                // Game over. Config integrity blown - we can't find strat.
                final String errorMsg = "Failed to find matching Strategy for Market " + market + " - The Strategy "
                        + "[" + strategyToUse + "] cannot be found in the " + " Strategy Descriptions map: "
                        + strategyDescriptions;
                LOG.error(errorMsg);
                throw new IllegalArgumentException(errorMsg);
            }
        }

        LOG.info(() -> "Loaded and set Market configuration successfully!");
    }
}