Perhaps a sign of the times: my most active Telegram chat is with a crypto-trading bot that constantly listens for opportunities to trade on my behalf. I used an open-source library to develop some strategies and configure the bot to execute them using my Binance account. The bot communicates all of its trades through Telegram and can reply to my requests to take action or share live updates.
15-second demo:
The Freqtrade library lets you code, backtest and perform optimization on a trading strategy the same way you would a machine learning model. This post will walk through some essential features of the bot and how to fine-tune strategies, without going into too much detail about every configuration/implementation step — there are plenty of tutorials that do a great job, and they're linked in the Resources section at the bottom.
Freqtrade uses TA-Lib (Technical Analysis Library), which is also an open-source library used by trading software and professionals to perform technical analysis on financial market data (not specific to cryptocurrencies). It includes about 200 indicators such as Bollinger Bands, RSI, MACD and more.
THE RECIPE
Freqtrade's documentation contains instructions on how to configure your Telegram bot and connect to your exchange of choice (Binance or Bittrex), but the core of it all is the strategy itself. The process can be summarized in 10 steps:
Let's dive into each step:
1. Implement a strategy
There are many out-of-the-box strategies you can use, but you can choose to have them as a starting point and then do your own tuning, polishing and optimization. For my bot, I started out with an existing strategy and iterated on it by selecting my own technical analysis indicators, configuring buy and sell rules and running the optimizer while tweaking a small number of variables at a time.
Every strategy has a skeleton with the functions
populate_indicators
, populate_buy_trend
and populate_sell_trend
This is what the
populate_indicators
function looks like for the strategy I used, Strategy002 (from the freqtrade/strategies repository) def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Adds several different TA indicators to the given DataFrame
Performance Note: For the best performance be frugal on the number of indicators
you are using. Let uncomment only the indicator you are using in your strategies
or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
"""
# Stoch
stoch = ta.STOCH(dataframe)
dataframe['slowk'] = stoch['slowk']
# RSI
dataframe['rsi'] = ta.RSI(dataframe)
# Inverse Fisher transform on RSI, values [-1.0, 1.0] (https://goo.gl/2JGGoy)
rsi = 0.1 * (dataframe['rsi'] - 50)
dataframe['fisher_rsi'] = (numpy.exp(2 * rsi) - 1) / (numpy.exp(2 * rsi) + 1)
# Bollinger bands
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=4)
dataframe['bb_lowerband'] = bollinger['lower']
# SAR Parabol
dataframe['sar'] = ta.SAR(dataframe)
# Hammer: values [0, 100]
dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe)
return dataframe
Out of the box, the strategy uses the following indicators:
2. Dry-run the initial strategy
Dry-runs are very effective in testing the code itself as well as the soundness of a strategy. All one has to do to dry-run a strategy is to set
"dry_run": false
in their config.json file.A bot in dry-run mode behaves just like it would in production, but all the trades will be simulated. You will still be able to communicate with your bot and monitor all its activity.
3. Backtest the initial strategy
Backtesting is very easy once you have the data you want. All the steps are specified in the Freqtrade documentation:
freqtrade download-data --exchange-binance
freqtrade backtesting --strategy <StrategyName>
4. Plot the strategy
Plotting can help you visualize the bot's buy/sell activity, price movement, as well as indicators against an asset pair.
The follownig plot contains ETC/BTC prices along with the bollinger bands and fisher RSI.
To plot this, we run:
freqtrade --strategy <StrategyName> plot-dataframe -p ETH/BTC --indicators1 bb_lowerband --indicators2 fisher_rsi
5. Hyperparameter Optimization
This is a key step in systematically improving any strategy, pre-packaged or not. Similar to tuning a machine learning model, we will be running a process to optimize on a loss function such as the Sharpe ratio or pure profit. The Freqtrade library uses algorithms from the scikit-optimize package to accomplish this.
The strategy itself is defined in one Python module, and the optimizer is in another. A hyperopt process consists of the following parts:
indicator_space()
and populate_buy_trend()
based on buy strategysell_indicator_space()
and populate_sell_trend()
based on our sell strategyDefaultHyperOptLoss
, SharpeHyperOptLoss
, OnlyProfitHyperOptLoss
, or write your ownThe
populate_indicators()
in the hyperopt file will the be same as the actual strategy's, as will populate_buy_trend()
and populate_sell_trend()
When it comes to indicators, we're asking for the optimizer to randomly combine to find the best combination of them. Here's an example definition of
indicator_space()
(there will be one for buy and one for sell): def indicator_space() -> List[Dimension]:
"""
Define your Hyperopt space for searching buy strategy parameters.
"""
return [
Integer(10, 25, name='mfi-value'),
Integer(20, 40, name='rsi-value'),
Categorical([True, False], name='mfi-enabled'),
Categorical([True, False], name='rsi-enabled'),
Categorical(['bb_lower'], name='trigger')
]
Two of them are
Integer
values (mfi-value
, rsi-value
), which means that the optimizer will give us the best values to use for those. Three
Categorical
variables: First two are either True or False: Use these to enable or disable the MFI and RSI guards. The last one is for triggers, which will decide which buy trigger we want to use. We do the same for the sell indicator space.Run the optimizer, specifying the hyperopt class, loss function class, number of epochs, and optionally the time range. The
--spaces all
is asking the optimizer to look at all possible parameters. But you can actually limit the set, as the optimizer is quite customizable. Let it run for many epochs, i.e. 1000.freqtrade hyperopt --customhyperopt SorosHyperopt --hyperopt-loss OnlyProfitHyperOptLoss -e 1000 --spaces all
After running the hyperopt script for 1000 epochs, I got the following results:
Buy hyperspace params:
{ 'mfi-enabled': False,
'mfi-value': 30,
'rsi-enabled': True,
'rsi-value': 17,
'slowk-enabled': False,
'slowk-value': 10,
'trigger': 'bb_lower3'}
Sell hyperspace params:
{ 'sell-fisher-rsi-enabled': True,
'sell-fisher-rsi-value': -0.37801505839606786,
'sell-sar-enabled': True,
'sell-sar-value': -0.8827367296210875}
ROI table:
{0: 0.07223, 29: 0.0288, 82: 0.01703, 159: 0}
Stoploss: -0.0865
Interpretation:
minimal_roi
value in your strategy6. Implement results from hyperopt
Once you have the recommendations from hyperopt, you want to update the populate_buy_trend and populate_sell_trend in your actual strategy (not the hyperopt file) so that next time you run
freqtrade --strategy <StrategyName>
, it will execute the optimized strategy. The hyperopt results above would translate to the following buy and sell strategies:
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators, populates the buy signal for the given dataframe
:param dataframe: DataFrame
:return: DataFrame with buy column
"""
dataframe.loc[
(
(dataframe['rsi'] < 17) &
(dataframe['bb_lowerband'] > dataframe['close']) # where dataframe['bb_lowerband'] is 3 stdev
),
'buy'] = 1
return dataframe
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators, populates the sell signal for the given dataframe
:param dataframe: DataFrame
:return: DataFrame with buy column
"""
dataframe.loc[
(
(dataframe['sar'] > -0.8827367296210875) &
(dataframe['fisher_rsi'] > -0.37801505839606786)
),
'sell'] = 1
return dataframe
7. Dry-run the new strategy and interpret the performance
Just like we did with the default strategy in the beginning, dry-run your new strategy to see how it performs!
8. Backtest the new strategy
Just as you did with the initial strategy, run
freqtrade backtesting --strategy <StrategyName>
The last line shows us how the bot did overall. Looks like we have a total profit of 3.55%
For more on how to interpret the backtesting results, read this page in the Freqtrade documentation.
9. Walk-forward analysis on new strategy
There are a few ways to avoid overfitting:
From the user_data/data/<exchange>/ folder, get the latest data from a data file (the last candle).
For example, the last line in user_data/data/binance/LTC_BTC-1m.json is:
[1567380600000,0.006791,0.006791,0.006791,0.006791,1.06]
Run the hyperparameter optimizer (hyperopt) on a specific timerange, by specifying two dates with the --timerange parameter
--timerange=20190701-20190815
$ freqtrade hyperopt --customhyperopt <HyperoptName> --hyperopt-loss SharpeHyperOptLoss -e 50 --spaces all --timerange=20190701-20190815
If you get an error about missing data, make sure you actually downloaded the data for the ranges you specified.
10. Enable live trading
All you have to do is flip a switch: Set
dry_run
to false
You may also choose to tune some parameters like `bid_strategy`(strategy when buying) which are less relevant in simulations but could apply in live scenarios:
For example, you can set
use_order_book: true
. This allows buying of pair using rates in order book bids. The ask price is how much someone is willing to sell an asset for, and the bid price is how much someone is willing to buy for. Turning this flag on allows you to buy at the bid price instead of ask. Sometimes the bid price will be lower than the ask, as opposed to ask = bid.Ready to run? Trigger freqtrade with your optimized and tested strategy, and go to your Binance account to monitor the trades!
VARIABLES THAT AFFECT OUTCOMES
Here is a non-exhaustive list of independent variables — values that when changed, can influence the profit/loss:
RESOURCES