Strategies – Framework [shortlists]

In the last chapter working-with-assets, we introduced manually adding assets to trade, along with scanning where we hinted an issue scanning large market lists. Take the Binance spot market with well over 1000 symbols. Keeping true to our pro plan with 2 calls per second, will give us a scan time of 1000 assets / 2 calls per second = 500 seconds, which equals 8 minutes and 20 seconds. This is getting into the “unacceptable scan time territory”.

Shortlists

Again sticking to our 1h scans for the RSI and MACD reversal signals, we might have to shorten this list of 1000+ symbols. Lets imagine that this strategy is only relevant if the daily close price just crossed up and above the 30 period Exponential Moving Average (EMA30).

In this case, we can create a shortlist, for which we will scan our hourly RSI + MACD setups.

For this we will create a new bot that will run in parallel to our Phobos bot. We’ll give this a more generic name, one that might be relevant to other bots as well. We’ll call this, the shortlist-daily-ema30-cross bot!

Lets first review the strategy configuration:

shortlist:
    id: "daily-ema30-cross" # The ID of the shortlist
    emaPeriod: 30 # Extra custom parameter for the shortlist scanner

bot:
    id: "shortlist-daily-ema30-cross" # The ID of the bot, notice that it's the same name as the shortlist ID, but prefixed with "shortlist-"
    fsm:
        isEnabled: false # Disable the FSM, as this bot only scans the market to create the shortlist
    scanner:
        isEnabled: true
        assetDelay: 500 # How big a delay between assets in milliseconds
        tick:
            schedule: "1 1 0 * * *" # Run the scanner every day at 00:01:01

trading:
    liveEnabled: false

Here, we’re adding some new items to the configuration:

  • shortlist
    • id: This is the id for which all items in the shortlist will be saved under. This shortlist will live in its own collection in MongoDB, under the name: <any-collection-prefix>shortlist-<shortlist-id>
    • emaPeriod: We can add any configuration we want to both the config.yml and this strategy.yml, and this will be available in the config objects

These new configuration settings aren’t actually used by the framework itself. We’re simply adding this information here, as it belongs in configuration files. And we’re simply just using these configuration parameters in our scanner.

Rate limits

We’re getting into a small pickle here regarding rate-limits. Because notice that this daily shortlist scanner will run for 8 minutes and 20 seconds at 1 second past the first minute after midnight (00:01:01), but so is the Phobos scanner. For this we have a couple of solutions:

  • We can reschedule our Phobos bot to only run every 15 minutes: 1 */15 * * * *, giving us enough time to complete the shortlist scan in between.
  • Upgrade to the expert plan, giving us 5 calls per second, instead of 2.
  • Finally, we can create some custom exception in Phobos, telling it to ignore the FSM and Scanner ticks for 10 minutes at every midnight, however, this is out of the scope of this guide.

Lets go with the first option, and change this shortlist scanner tick schedule to 5 minutes past midnight: 1 5 0 * * *, which is when Phobos should be all done. Now, obviously, this depends how many assets the Phobos scanner is picking up.

Scanner

Our scanner implementation will look as follows:

// Create a new Scanner class
class Scanner {
    
    // Create a constructor that accepts the config, database, utilities, taapiClient, order, and notifications objects
    constructor(config, database, utilities, taapiClient, order, notifications) {
        this.config = config;
        this.database = database;
        this.utilities = utilities;
        this.taapiClient = taapiClient;
        this.order = order;
        this.notifications = notifications;
    }

    // Mandatory scan() function
    async scan() {

        // Get all pairs from the exchange through TAAPI
        this.taapiClient.getNpmClient().getExchangeSymbols("crypto", this.config.exchange.id).then( async symbols => {

            // Loop through each symbol
            for(let s in symbols) {

                // Get the symbol
                let symbol = symbols[s];

                // Reset bulk queires
                this.taapiClient.resetBulkConstructs();

                // Get latest closed candle
                this.taapiClient.addCalculation("candle", symbol, "1d", "candle_1d", { backtrack: 1 });

                // Add EMA30 calculation
                this.taapiClient.addCalculation("ema", symbol, "1d", "ema30_1d", { backtrack: 1, period: this.config.shortlist.emaPeriod });

                // Fetch all calculations
                await this.taapiClient.executeBulk().then( async ta => {

                    // If debug mode is enabled, log the scan item
                    if(this.config.server.debugMode) {
                        console.log(`Examining ${s}:${symbol}...`);
                    }

                    // If the open price is less than the EMA30 and the close 
                    // price is greater than the EMA30, then we have a crossover
                    if(ta.candle_1d.open < ta.ema30_1d.value && ta.candle_1d.close > ta.ema30_1d.value) {

                        // Add asset to shortlist
                        this.database.setShortlistItem(this.config.shortlist.id, symbol);

                    } else {

                        // Remove asset from shortlist
                        this.database.deleteShortlistItem(this.config.shortlist.id, symbol);

                    }
                });

                // Wait for the configured delay between each asset (rate-limits by TAAPI)
                await this.utilities.sleep(this.config.bot.scanner.assetDelay);                
            };
        });
    }
}

module.exports = Scanner;

With this, we’ve created a new bot, with its own scanner, simply to create a shortlist. Lets see how a bot can use this shortlist, instead of scanning an entire market.

For this we’ll create yet another bot, and we’ll name this Deimos, the second moon of Mars. Lets have a look at the configuration first:

bot:
    id: "deimos"
    fsm:
        isEnabled: true
        tradeDelay: 500
        tick:
            schedule: "1 */15 * * * *"
    scanner:
        isEnabled: true
        assetDelay: 500
        shortlist:
            id: "daily-ema30-cross"
            dbName: "strategies_shortlist-daily-ema30-cross" # Note we need the full DB name including all prefixes, as the configuration for another bot might differ from this bot!
        tick:
            schedule: "1 5 * * * *"

trading:
    liveEnabled: false

So we’re simply defining some properties for Deimos, and telling to run the FSM every 15 minutes on the hour, and the scanner 5 minutes past every hour.

Implementation of the Deimos scanner:

// Create a new Scanner class
class Scanner {
    
    // Create a constructor that accepts the config, database, utilities, taapiClient, order, and notifications objects
    constructor(config, database, utilities, taapiClient, order, notifications) {
        this.config = config;
        this.database = database;
        this.utilities = utilities;
        this.taapiClient = taapiClient;
        this.order = order;
        this.notifications = notifications;
    }

    // Mandatory scan() function
    async scan() {

        /**
         * Fetch the shortlist using the configured shortlist ID and database name.
         * 
         * The shortlist ID is used to identify the shortlist in the database. The next
         * parameter is the query, which is used to filter the results. The third parameter
         * is the sort, which is used to sort the results. The final parameter is the database
         * name, which tells us that this shortlist is stored under a different bot.
         */ 
        let shortlist = await this.database.getShortlist(this.config.bot.scanner.shortlist.id, {}, {}, this.config.bot.scanner.shortlist.dbName);
        
        // Loop through each shortlist item
        for(let s in shortlist) {

            // Get the symbol
            let symbolInfo = shortlist[s];

            // Reset bulk queires
            this.taapiClient.resetBulkConstructs();

            // Add RSI calculation
            this.taapiClient.addCalculation("rsi", symbolInfo.symbol, "1h", "rsi_1h");

            // Add MACD calculation
            this.taapiClient.addCalculation("macd", symbolInfo.symbol, "1h", "macd_1h");

            // Fetch all calculations
            await this.taapiClient.executeBulk().then( async ta => {

                // If debug mode is enabled, log the scan item
                if(this.config.server.debugMode) {
                    console.log(`Examining ${s}:${symbolInfo.symbol}...`);
                }

                // If the RSI is less than 30 and the MACD is greater than the MACD Signal
                // then create the trade
                if(ta.rsi_1h.value < 30 && ta.macd_1h.valueMACD > ta.macd_1h.valueMACDSignal) {
                    await this.database.createTrade(this.config.exchange.id, symbolInfo.symbol, "1h");

                    // Post a message to Slack
                    this.notifications.postSlackMessage(`Found trade: ${symbolInfo.symbol} on ${this.config.exchange.id} on 1h.`);
                }
            });

            // Wait for the configured delay between each asset (rate-limits by TAAPI)
            await this.utilities.sleep(this.config.bot.scanner.assetDelay);
        }

        this.notifications.postSlackMessage(`Scanning complete.`);        
        
    }
}

module.exports = Scanner;

Note

It’s important to note that all the ‘shortlist’ configurations, aren’t in any place hardcoded. It’s simply a suggestion to place the shortlist config in some place. In fact, you could even make several shortlists within the same bot if that has a use-case within the same tick timing. When fetching and storing shortlist items, a shortlist ID will have to be provided to the method database.setShortlistItem(this.config.shortlist.id, symbol); So you may place this config where ever you see fit, or even skip it all together.

Conclusion

And with that, we’ve created a shortlist to shorten a list of symbols in the thousands, down to way fewer symbols. This allows us to very quickly scan through more complex TA, which additionally might be more time sensitive to get into a position sooner than later when a new hourly candle forms.

Lets now get into some hosting, as we’d really like to have our bot(s) running in the cloud and not our local machine.