> mkdir -p taapi-strategies/strategy
> cd taapi-strategies/strategy
Now let’s initiate a NodeJS project, and install TAAPI.IO Strategies.
> npm init
> npm install --save taapi-strategies
Directory / files Structure
Below you’ll see the folder and files the framework expects.
taapi-strategies/
|-- strategy/
|-- node_modules
|-- bot/
| |-- README.md
| |-- State_<your-state-name>.js
| |-- State_start.js
| |-- State.js
| |-- strategy.yml
|
|-- src/
| |-- index.js
|
|-- config.yml
|-- package-lock.json
|-- package.json
At this point, you should already see the node_modules
, package-lock.json
and package.json
files and folders.
Lets create the remaining items.
> touch config.yml # This creates the config file
> mkdir bot # Create bot folder
> mkdir src # Create src folder
> cd src
> touch index.js # Create index file in src folder
And boot up our Code IDE
> code .
Paste in configuration.
server:
env: "dev"
host: "localhost"
port: 3002
debugMode: true
api:
corsOrigin: "*"
user: "user"
pass: "pass"
database:
namePrefix: "taapi_strategies_"
name: "${BOT_ID}"
host: "mongo"
port: 27017
user: ""
pass: ""
protocol: "mongodb"
collectionPrefix: ""
replSetName: null
sslValidate: false
sslCertFilename: null
exchange:
id: "binancefutures"
name: "Binance Futures"
logging:
errors: true
stateChanges: true
ta: false
uptimerobot:
heartbeatUrl: ""
taapi:
secret: ""
notifications:
slack:
webhookUrl: null
channel: "bot"
username: "TAAPI.IO Strategies"
icon: "information_source"
telegram:
bot_key: null
Important: Dont forget to paste in your TAAPI.IO Secret above!
Paste in index file content
// Require taapi-strategies
const TaapiStrategies = require('taapi-strategies');
// Create a new instance of taapi-strategies
const app = new TaapiStrategies.default();
// Start the app
app.start({
"start-bot": true, // Boots up the Finite State Machine
"start-api": true // Boots up a REST API so that we can talk to our bot
});
We’re now done with the main installation and setup. Now let’s get going with our strategy.
Strategy
We will now implement whats with a fancy name, called a “Finite State Machine“. This is a common implementation of the “State Design Pattern“, and the Refactoring Guru did a good job describing this. Our MongoDB database is then used to keep track of the states we’re in.
So in a nut-shell, we have here 3 states well described by their names:
- Start – This is the start state when the program boots up the first time
- Long – We’re invested in a long position
- Short – We’re invested in a short position
And the transitions (lines) describe how we move between states.
This “machine logic” is applicable for every asset we’d like to analyse and potentially trade. So say that we’re looking at BTC/USDT on the 1 hour timeframe from Binance, we can easily analyse the 2 EMAs.
In our bot
folder, let’s create 6 files:
- README.md – A Markdown description of your bot
- State.js # All state implementations will inherit from this file
- State_start.js
- State_long.js
- State_short.js
- strategy.yml – Configuration for this unique strategy
Here’s the implementation / content for each file. We will name this strategy after one of the moons of Mars: “Phobos“
# Phobos
## Description
Simple bare bone EMA crossover strategy.
Scans the 'shortlist' for EMA crossover setups, long and short.
| Property | Value | Notes |
|-------------|-----------------------|----------------------------------------|
| Timeframe | 1h | |
| Risk/Reward | N/A | |
| Target | Reverse EMA Crossover | |
| Stoploss | Reverse EMA Crossover | |
const BotState = require("taapi-strategies/dist/fsm/BotState.js").default;
class State extends BotState
{
constructor(trade, config, database, order) {
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;
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;
const State = require("./State.js");
class State_long 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}`);
}
await this.executeBulk().then( ta => {
if(ta.ema_9 < ta.ema_20) {
// Exit position
this.exitPosition().then( exitPositionResult => {
// Verify that whole position is closed
if(exitPositionResult.success) {
// Enter new opposite 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_long;
const State = require("./State.js");
class State_short 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) {
// Exit position
this.exitPosition().then( exitPositionResult => {
// Verify that whole position is closed
if(exitPositionResult.success) {
// Enter new opposite 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");
}
});
}
});
}
});
}
}
module.exports = State_short;
bot:
id: "phobos"
fsm:
isEnabled: true
tradeDelay: 1000
tick:
schedule: "15 */5 * * * *"
scanner:
isEnabled: false
trading:
liveEnabled: false
Please go through the above lines of code, especially the ones for states [start, long, short]. It should be a fairly easy read, even for not so experienced coders.
Run
We’re now ready to run our bot
> node src/index.js
Conclusion / Next steps
This article was mainly intended to show how the bits and pieces are stitched together. There’re several things, which are not “optimal” in the above setup.
So let’s improve this, but actually at the same time, make things a bit easier using a couple of tools. Please visit the next chapter where we will properly set things up cloning a git repository, and running multiple bots with Docker.