To get development started, lets review our basic Exponential Moving Average Crossover strategy. We will refer to this strategy using the name “Phobos”, one of the moons of Mars.

Strategy | Phobos |
Symbol | BTC/USDT |
Interval | 1 Hour [1h], latest close |
Exchange | Binance Futures |
Fast EMA | 9 period |
Slow EMA | 20 period |
When the latest closed fast EMA (9) crosses up and over the slow EMA (20), we will treat this as a bullish signal and thus go LONG, and vice-versa when the EMA9 crosses down below the EMA20, we will close our long position (if any), and enter a new SHORT position.
Engine
FSM

The engine consists of 2 main parts, the actual Finite State Machine, and a Scanner. For this part, we’ll focus on the FSM.
As you can see, there’s a timer that runs off of a Cron Job. In this example, we set our timer to execute 1 second past every 5 minutes. Why 5 minutes when running on the hourly? Well, not really needed, but as we’ll see in the later UI chapter, it’s nice to have data updated a bit more often than every hour.
Have a look at the strategy configuration. This is where we configure this “Tick Schedule” for the FSM.
bot:
id: "phobos"
fsm:
isEnabled: true
tradeDelay: 1000 # How big a delay between trades in milliseconds
tick:
schedule: "1 */5 * * * *"
scanner:
isEnabled: false
trading:
liveEnabled: false
Now, at every 1 second past every 5 minutes [00:00:01, 00:05:01, 00:10:01, 17:40:01 etc…] the timer kicks in, and fetches all active trades for this strategy in the database and starts to loop through these. Every trade consists of a [symbol, interval, state, among other things]. Thus, if say BTC/USDT is in a long trade, every time the timer is triggered, the code in State_long.js
is then executed. And note the above tradeDelay. If you’re trading multiple assets in this same strategy, the FSM engine waits 1000ms before executing the next trade (or symbol). So say you’re also trading Litecoin to the Dollar, then this LTC/USDT symbol will execute at 2 seconds past every 5 minutes, thus respecting your TAAPI.IO rate-limits.
We’ll now go through the implementation of each state, starting with the parent State class
// Require the base framework state class
const BotState = require("taapi-strategies/dist/fsm/BotState.js").default;
// Create a new state class
class State extends BotState
{
// Constructor
constructor(trade, config, database, order) {
// Call parent constructor
super(trade, config, database, order);
// Get current price
this.addCalculation("price", trade.interval, `price`);
// Get latest closed EMA 9 value
this.addCalculation("ema", trade.interval, "ema_9", { period: 9, backtrack: 1 });
// Get latest closed EMA 20 value
this.addCalculation("ema", trade.interval, "ema_20", { period: 20, backtrack: 1 });
}
async tick() {
throw new Error("Method 'tick()' must be implemented!");
}
}
module.exports = State;
What happens here is that the first line requires or includes the entire framework, where all tools, functions etc… are located. We then create a new class and extend this to the framework.
The constructor part of the code, is what’s called and run every time a new instance of the class is created. So the first ‘super’ function call is to let the framework know about our trade info, configuration, database and order. Next we’re adding 3 indicators to a bulk call. If your TAAPI.IO plan allows for Multiple Constructs, you can even add other assets / timeframes in the same request.
The tick() function simply tells that the child class must implement this tick() function.
Start
const State = require("./State.js");
class State_start extends State
{
constructor(trade, config, database, order) {
super(trade, config, database, order);
}
async tick() {
if(this.config.server.debugMode) {
console.log(`Tick - ${this.trade.state}:${this.trade.symbol}:${this.trade.interval}`);
}
this.executeBulk().then( ta => {
if(ta.ema_9 > ta.ema_20) {
// Enter long position
this.enterPosition("LONG", ta.price).then( enterPositionResult => {
// Verify that whole position is filled and take profit and stoploss orders are placed
if(enterPositionResult.success) {
this.changeState("long");
}
});
} else if(ta.ema_9 < ta.ema_20) {
// Enter short position
this.enterPosition("SHORT", ta.price).then( enterPositionResult => {
// Verify that whole position is filled and take profit and stoploss orders are placed
if(enterPositionResult.success) {
this.changeState("short");
}
});
}
});
}
}
module.exports = State_start;
Scanner
Conclusion