diff --git a/.gitignore b/.gitignore index c81b55222..7c7102874 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ config.json *.sqlite .hyperopt logfile.txt +hyperopt_trials.pickle +user_data/ # Byte-compiled / optimized / DLL files __pycache__/ @@ -85,5 +87,3 @@ target/ .venv .idea .vscode - -hyperopt_trials.pickle diff --git a/config.json.example b/config.json.example index 0f4271001..37980447d 100644 --- a/config.json.example +++ b/config.json.example @@ -4,7 +4,7 @@ "stake_amount": 0.05, "fiat_display_currency": "USD", "dry_run": false, - "ticker_interval": "5", + "ticker_interval": 5, "minimal_roi": { "40": 0.0, "30": 0.01, diff --git a/docs/bot-optimization.md b/docs/bot-optimization.md index 424bab989..7900e6dd2 100644 --- a/docs/bot-optimization.md +++ b/docs/bot-optimization.md @@ -3,21 +3,55 @@ This page explains where to customize your strategies, and add new indicators. ## Table of Contents -- [Change your strategy](#change-your-strategy) +- [Install a custom strategy file](#install-a-custom-strategy-file) +- [Customize your strategy](#change-your-strategy) - [Add more Indicator](#add-more-indicator) +- [Where is the default strategy](#where-is-the-default-strategy) + +Since the version `0.16.0` the bot allows using custom strategy file. + +## Install a custom strategy file +This is very simple. Copy paste your strategy file into the folder +`user_data/strategies`. + +Let assume you have a strategy file `awesome-strategy.py`: +1. Move your file into `user_data/strategies` (you should have `user_data/strategies/awesome-strategy.py` +2. Start the bot with the param `--strategy awesome-strategy` (the parameter is the name of the file without '.py') + +```bash +python3 ./freqtrade/main.py --strategy awesome_strategy +``` ## Change your strategy -The bot is using buy and sell strategies to buy and sell your trades. -Both are customizable. +The bot includes a default strategy file. However, we recommend you to +use your own file to not have to lose your parameters everytime the default +strategy file will be updated on Github. Put your custom strategy file +into the folder `user_data/strategies`. -### Buy strategy -The default buy strategy is located in the file -[freqtrade/analyze.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/analyze.py#L73-L92). -Edit the function `populate_buy_trend()` to update your buy strategy. +A strategy file contains all the information needed to build a good strategy: +- Buy strategy rules +- Sell strategy rules +- Minimal ROI recommended +- Stoploss recommended +- Hyperopt parameter -Sample: +The bot also include a sample strategy you can update: `user_data/strategies/test_strategy.py`. +You can test it with the parameter: `--strategy test_strategy` + +```bash +python3 ./freqtrade/main.py --strategy awesome_strategy +``` + +**For the following section we will use the [user_data/strategies/test_strategy.py](https://github.com/gcarq/freqtrade/blob/develop/user_data/strategies/test_strategy.py) +file as reference.** + +### Buy strategy +Edit the method `populate_buy_trend()` into your strategy file to +update your buy strategy. + +Sample from `user_data/strategies/test_strategy.py`: ```python -def populate_buy_trend(dataframe: DataFrame) -> DataFrame: +def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: """ Based on TA indicators, populates the buy signal for the given dataframe :param dataframe: DataFrame @@ -25,14 +59,9 @@ def populate_buy_trend(dataframe: DataFrame) -> DataFrame: """ dataframe.loc[ ( - (dataframe['rsi'] < 35) & - (dataframe['fastd'] < 35) & (dataframe['adx'] > 30) & - (dataframe['plus_di'] > 0.5) - ) | - ( - (dataframe['adx'] > 65) & - (dataframe['plus_di'] > 0.5) + (dataframe['tema'] <= dataframe['blower']) & + (dataframe['tema'] > dataframe['tema'].shift(1)) ), 'buy'] = 1 @@ -40,41 +69,31 @@ def populate_buy_trend(dataframe: DataFrame) -> DataFrame: ``` ### Sell strategy -The default buy strategy is located in the file -[freqtrade/analyze.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/analyze.py#L95-L115) -Edit the function `populate_sell_trend()` to update your buy strategy. +Edit the method `populate_sell_trend()` into your strategy file to +update your sell strategy. -Sample: +Sample from `user_data/strategies/test_strategy.py`: ```python -def populate_sell_trend(dataframe: DataFrame) -> DataFrame: +def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame: """ Based on TA indicators, populates the sell signal for the given dataframe :param dataframe: DataFrame :return: DataFrame with buy column """ dataframe.loc[ - ( - ( - (crossed_above(dataframe['rsi'], 70)) | - (crossed_above(dataframe['fastd'], 70)) - ) & - (dataframe['adx'] > 10) & - (dataframe['minus_di'] > 0) - ) | ( (dataframe['adx'] > 70) & - (dataframe['minus_di'] > 0.5) + (dataframe['tema'] > dataframe['blower']) & + (dataframe['tema'] < dataframe['tema'].shift(1)) ), 'sell'] = 1 return dataframe ``` ## Add more Indicator -As you have seen, buy and sell strategies need indicators. You can see -the indicators in the file -[freqtrade/analyze.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/analyze.py#L95-L115). -Of course you can add more indicators by extending the list contained in -the function `populate_indicators()`. +As you have seen, buy and sell strategies need indicators. You can add +more indicators by extending the list contained in +the method `populate_indicators()` from your strategy file. Sample: ```python @@ -111,6 +130,15 @@ def populate_indicators(dataframe: DataFrame) -> DataFrame: return dataframe ``` +**Want more indicators example?** +Look into the [user_data/strategies/test_strategy.py](https://github.com/gcarq/freqtrade/blob/develop/user_data/strategies/test_strategy.py). +Then uncomment indicators you need. + + +### Where is the default strategy? +The default buy strategy is located in the file +[freqtrade/default_strategy.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/strategy/default_strategy.py). + ## Next step Now you have a perfect strategy you probably want to backtesting it. diff --git a/docs/bot-usage.md b/docs/bot-usage.md index d3dcf8659..ca92d74c4 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -22,19 +22,21 @@ positional arguments: optional arguments: -h, --help show this help message and exit - -c PATH, --config PATH - specify configuration file (default: config.json) -v, --verbose be verbose --version show program's version number and exit - -dd PATH, --datadir PATH - Path is from where backtesting and hyperopt will load the - ticker data files (default freqdata/tests/testdata). - --dynamic-whitelist [INT] - dynamically generate and update whitelist based on 24h - BaseVolume (Default 20 currencies) + -c PATH, --config PATH + specify configuration file (default: config.json) + -s PATH, --strategy PATH + specify strategy file (default: + freqtrade/strategy/default_strategy.py) --dry-run-db Force dry run to use a local DB "tradesv3.dry_run.sqlite" instead of memory DB. Work only if dry_run is enabled. + -dd PATH, --datadir PATH + path to backtest data (default freqdata/tests/testdata + --dynamic-whitelist [INT] + dynamically generate and update whitelist based on 24h + BaseVolume (Default 20 currencies) ``` ### How to use a different config file? @@ -45,6 +47,33 @@ default, the bot will load the file `./config.json` python3 ./freqtrade/main.py -c path/far/far/away/config.json ``` +### How to use --strategy? +This parameter will allow you to load your custom strategy file. Per +default without `--strategy` or `-s` the bol will load the +`default_strategy` included with the bot (`freqtrade/strategy/default_strategy.py`). + +The bot will search your strategy file into `user_data/strategies` and +`freqtrade/strategy`. + +To load a strategy, simply pass the file name (without .py) in this +parameters. + +**Example:** +In `user_data/strategies` you have a file `my_awesome_strategy.py` to +load it: +```bash +python3 ./freqtrade/main.py --strategy my_awesome_strategy +``` + +If the bot does not find your strategy file, it will fallback to the +`default_strategy`. + +Learn more about strategy file in [optimize your bot](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-optimization.md). + +#### How to install a strategy? +This is very simple. Copy paste your strategy file into the folder +`user_data/strategies`. And voila, the bot is ready to use it. + ### How to use --dynamic-whitelist? Per default `--dynamic-whitelist` will retrieve the 20 currencies based on BaseVolume. This value can be changed when you run the script. diff --git a/docs/configuration.md b/docs/configuration.md index 7a41644ee..84c8f53ba 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -17,11 +17,11 @@ The table below will list all configuration parameters. | `max_open_trades` | 3 | Yes | Number of trades open your bot will have. | `stake_currency` | BTC | Yes | Crypto-currency used for trading. | `stake_amount` | 0.05 | Yes | Amount of crypto-currency your bot will use for each trade. Per default, the bot will use (0.05 BTC x 3) = 0.15 BTC in total will be always engaged. -| `ticker_interval` | ["1", "5", "30, "60", "1440"] | No | The ticker interval to use (1min, 5 min, 30 min, 1 hour or 1 day). Defaut is 5 minutes +| `ticker_interval` | [1, 5, 30, 60, 1440] | No | The ticker interval to use (1min, 5 min, 30 min, 1 hour or 1 day). Defaut is 5 minutes | `fiat_display_currency` | USD | Yes | Fiat currency used to show your profits. More information below. | `dry_run` | true | Yes | Define if the bot must be in Dry-run or production mode. -| `minimal_roi` | See below | Yes | Set the threshold in percent the bot will use to sell a trade. More information below. -| `stoploss` | -0.10 | No | Value of the stoploss in percent used by the bot. More information below. +| `minimal_roi` | See below | No | Set the threshold in percent the bot will use to sell a trade. More information below. If set, this parameter will override `minimal_roi` from your strategy file. +| `stoploss` | -0.10 | No | Value of the stoploss in percent used by the bot. More information below. If set, this parameter will override `stoploss` from your strategy file. | `unfilledtimeout` | 0 | No | How long (in minutes) the bot will wait for an unfilled order to complete, after which the order will be cancelled. | `bid_strategy.ask_last_balance` | 0.0 | Yes | Set the bidding price. More information below. | `exchange.name` | bittrex | Yes | Name of the exchange class to use. @@ -53,11 +53,19 @@ See the example below: }, ``` +Most of the strategy files already include the optimal `minimal_roi` +value. This parameter is optional. If you use it, it will take over the +`minimal_roi` value from the strategy file. + ### Understand stoploss `stoploss` is loss in percentage that should trigger a sale. For example value `-0.10` will cause immediate sell if the profit dips below -10% for a given trade. This parameter is optional. +Most of the strategy files already include the optimal `stoploss` +value. This parameter is optional. If you use it, it will take over the +`stoploss` value from the strategy file. + ### Understand initial_state `initial_state` is an optional field that defines the initial application state. Possible values are `running` or `stopped`. (default=`running`) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index af564f0b6..3c3cb7d25 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -14,14 +14,13 @@ parameters with Hyperopt. ## Prepare Hyperopt Before we start digging in Hyperopt, we recommend you to take a look at -out Hyperopt file -[freqtrade/optimize/hyperopt.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py) +your strategy file located into [user_data/strategies/](https://github.com/gcarq/freqtrade/blob/develop/user_data/strategies/test_strategy.py) ### 1. Configure your Guards and Triggers -There are two places you need to change to add a new buy strategy for -testing: -- Inside the [populate_buy_trend()](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L167-L207). -- Inside the [SPACE dict](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L47-L94). +There are two places you need to change in your strategy file to add a +new buy strategy for testing: +- Inside [populate_buy_trend()](https://github.com/gcarq/freqtrade/blob/develop/user_data/strategies/test_strategy.py#L278-L294). +- Inside [hyperopt_space()](https://github.com/gcarq/freqtrade/blob/develop/user_data/strategies/test_strategy.py#L244-L297) known as `SPACE`. There you have two different type of indicators: 1. `guards` and 2. `triggers`. @@ -38,10 +37,10 @@ ADX > 10*". If you have updated the buy strategy, means change the content of -`populate_buy_trend()` function you have to update the `guards` and +`populate_buy_trend()` method you have to update the `guards` and `triggers` hyperopts must used. -As for an example if your `populate_buy_trend()` function is: +As for an example if your `populate_buy_trend()` method is: ```python def populate_buy_trend(dataframe: DataFrame) -> DataFrame: dataframe.loc[ @@ -56,10 +55,10 @@ Your hyperopt file must contains `guards` to find the right value for `(dataframe['adx'] > 65)` & and `(dataframe['plus_di'] > 0.5)`. That means you will need to enable/disable triggers. -In our case the `SPACE` and `populate_buy_trend` in hyperopt.py file +In our case the `SPACE` and `populate_buy_trend` in your strategy file will be look like: ```python -SPACE = { +space = { 'rsi': hp.choice('rsi', [ {'enabled': False}, {'enabled': True, 'value': hp.quniform('rsi-value', 20, 40, 1)} @@ -82,7 +81,7 @@ SPACE = { ... -def populate_buy_trend(dataframe: DataFrame) -> DataFrame: +def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: conditions = [] # GUARDS AND TRENDS if params['adx']['enabled']: @@ -111,13 +110,13 @@ cannot use your config file. It is also made on purpose to allow you testing your strategy with different configurations. The Hyperopt configuration is located in -[freqtrade/optimize/hyperopt_conf.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/optimize/hyperopt_conf.py). +[user_data/hyperopt_conf.py](https://github.com/gcarq/freqtrade/blob/develop/user_data/hyperopt_conf.py). ## Advanced notions ### Understand the Guards and Triggers When you need to add the new guards and triggers to be hyperopt -parameters, you do this by adding them into the [SPACE dict](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L47-L94). +parameters, you do this by adding them into the [hyperopt_space()](https://github.com/gcarq/freqtrade/blob/develop/user_data/strategies/test_strategy.py#L244-L297). If it's a trigger, you add one line to the 'trigger' choice group and that's it. @@ -149,9 +148,8 @@ for best working algo. ### Add a new Indicators If you want to test an indicator that isn't used by the bot currently, -you need to add it to -[freqtrade/analyze.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/analyze.py#L40-L70) -inside the `populate_indicators` function. +you need to add it to your strategy file (example: [user_data/strategies/test_strategy.py](https://github.com/gcarq/freqtrade/blob/develop/user_data/strategies/test_strategy.py)) +inside the `populate_indicators()` method. ## Execute Hyperopt Once you have updated your hyperopt configuration you can run it. @@ -165,8 +163,8 @@ python3 ./freqtrade/main.py -c config.json hyperopt ### Execute hyperopt with different ticker-data source If you would like to learn parameters using an alternate ticke-data that -you have on-disk, use the --datadir PATH option. Default hyperopt will -use data from directory freqtrade/tests/testdata. +you have on-disk, use the `--datadir PATH` option. Default hyperopt will +use data from directory `user_data/data`. ### Running hyperopt with smaller testset @@ -270,15 +268,11 @@ customizable value. - and so on... -You have to look from -[freqtrade/optimize/hyperopt.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L170-L200) -what those values match to. +You have to look inside your strategy file into `buy_strategy_generator()` +method, what those values match to. So for example you had `adx:` with the `value: 15.0` so we would look -at `adx`-block from -[freqtrade/optimize/hyperopt.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L178-L179). -That translates to the following code block to -[analyze.populate_buy_trend()](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/analyze.py#L73) +at `adx`-block, that translates to the following code block: ``` (dataframe['adx'] > 15.0) ``` @@ -286,7 +280,7 @@ That translates to the following code block to So translating your whole hyperopt result to as the new buy-signal would be the following: ``` -def populate_buy_trend(dataframe: DataFrame) -> DataFrame: +def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: dataframe.loc[ ( (dataframe['adx'] > 15.0) & # adx-value diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index d1671e4c6..cd4515a3b 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ FreqTrade bot """ -__version__ = '0.15.1' +__version__ = '0.16.0' class DependencyException(BaseException): diff --git a/freqtrade/analyze.py b/freqtrade/analyze.py index cee5d175a..0481c7f3c 100644 --- a/freqtrade/analyze.py +++ b/freqtrade/analyze.py @@ -7,11 +7,10 @@ from enum import Enum from typing import Dict, List import arrow -import talib.abstract as ta from pandas import DataFrame, to_datetime -import freqtrade.vendor.qtpylib.indicators as qtpylib from freqtrade.exchange import get_ticker_history +from freqtrade.strategy.strategy import Strategy logger = logging.getLogger(__name__) @@ -30,8 +29,9 @@ def parse_ticker_dataframe(ticker: list) -> DataFrame: """ columns = {'C': 'close', 'V': 'volume', 'O': 'open', 'H': 'high', 'L': 'low', 'T': 'date'} frame = DataFrame(ticker) \ - .drop('BV', 1) \ .rename(columns=columns) + if 'BV' in frame: + frame.drop('BV', 1, inplace=True) frame['date'] = to_datetime(frame['date'], utc=True, infer_datetime_format=True) frame.sort_values('date', inplace=True) return frame @@ -45,182 +45,8 @@ def populate_indicators(dataframe: DataFrame) -> DataFrame: 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. """ - - # Momentum Indicator - # ------------------------------------ - - # ADX - dataframe['adx'] = ta.ADX(dataframe) - - # Awesome oscillator - dataframe['ao'] = qtpylib.awesome_oscillator(dataframe) - """ - # Commodity Channel Index: values Oversold:<-100, Overbought:>100 - dataframe['cci'] = ta.CCI(dataframe) - """ - # MACD - macd = ta.MACD(dataframe) - dataframe['macd'] = macd['macd'] - dataframe['macdsignal'] = macd['macdsignal'] - dataframe['macdhist'] = macd['macdhist'] - - # MFI - dataframe['mfi'] = ta.MFI(dataframe) - - # Minus Directional Indicator / Movement - dataframe['minus_dm'] = ta.MINUS_DM(dataframe) - dataframe['minus_di'] = ta.MINUS_DI(dataframe) - - # Plus Directional Indicator / Movement - dataframe['plus_dm'] = ta.PLUS_DM(dataframe) - dataframe['plus_di'] = ta.PLUS_DI(dataframe) - dataframe['minus_di'] = ta.MINUS_DI(dataframe) - - """ - # ROC - dataframe['roc'] = ta.ROC(dataframe) - """ - # 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) - - # Inverse Fisher transform on RSI normalized, value [0.0, 100.0] (https://goo.gl/2JGGoy) - dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1) - - # Stoch - stoch = ta.STOCH(dataframe) - dataframe['slowd'] = stoch['slowd'] - dataframe['slowk'] = stoch['slowk'] - """ - # Stoch fast - stoch_fast = ta.STOCHF(dataframe) - dataframe['fastd'] = stoch_fast['fastd'] - dataframe['fastk'] = stoch_fast['fastk'] - """ - # Stoch RSI - stoch_rsi = ta.STOCHRSI(dataframe) - dataframe['fastd_rsi'] = stoch_rsi['fastd'] - dataframe['fastk_rsi'] = stoch_rsi['fastk'] - """ - - # Overlap Studies - # ------------------------------------ - - # Previous Bollinger bands - # Because ta.BBANDS implementation is broken with small numbers, it actually - # returns middle band for all the three bands. Switch to qtpylib.bollinger_bands - # and use middle band instead. - dataframe['blower'] = ta.BBANDS(dataframe, nbdevup=2, nbdevdn=2)['lowerband'] - """ - # Bollinger bands - """ - bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) - dataframe['bb_lowerband'] = bollinger['lower'] - dataframe['bb_middleband'] = bollinger['mid'] - dataframe['bb_upperband'] = bollinger['upper'] - - # EMA - Exponential Moving Average - dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3) - dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5) - dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10) - dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50) - dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100) - - # SAR Parabol - dataframe['sar'] = ta.SAR(dataframe) - - # SMA - Simple Moving Average - dataframe['sma'] = ta.SMA(dataframe, timeperiod=40) - - # TEMA - Triple Exponential Moving Average - dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9) - - # Cycle Indicator - # ------------------------------------ - # Hilbert Transform Indicator - SineWave - hilbert = ta.HT_SINE(dataframe) - dataframe['htsine'] = hilbert['sine'] - dataframe['htleadsine'] = hilbert['leadsine'] - - # Pattern Recognition - Bullish candlestick patterns - # ------------------------------------ - """ - # Hammer: values [0, 100] - dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe) - - # Inverted Hammer: values [0, 100] - dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe) - - # Dragonfly Doji: values [0, 100] - dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe) - - # Piercing Line: values [0, 100] - dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100] - - # Morningstar: values [0, 100] - dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100] - - # Three White Soldiers: values [0, 100] - dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100] - """ - - # Pattern Recognition - Bearish candlestick patterns - # ------------------------------------ - """ - # Hanging Man: values [0, 100] - dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe) - - # Shooting Star: values [0, 100] - dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe) - - # Gravestone Doji: values [0, 100] - dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe) - - # Dark Cloud Cover: values [0, 100] - dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe) - - # Evening Doji Star: values [0, 100] - dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe) - - # Evening Star: values [0, 100] - dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe) - """ - - # Pattern Recognition - Bullish/Bearish candlestick patterns - # ------------------------------------ - """ - # Three Line Strike: values [0, -100, 100] - dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe) - - # Spinning Top: values [0, -100, 100] - dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100] - - # Engulfing: values [0, -100, 100] - dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100] - - # Harami: values [0, -100, 100] - dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100] - - # Three Outside Up/Down: values [0, -100, 100] - dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100] - - # Three Inside Up/Down: values [0, -100, 100] - dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100] - """ - - # Chart type - # ------------------------------------ - # Heikinashi stategy - heikinashi = qtpylib.heikinashi(dataframe) - dataframe['ha_open'] = heikinashi['open'] - dataframe['ha_close'] = heikinashi['close'] - dataframe['ha_high'] = heikinashi['high'] - dataframe['ha_low'] = heikinashi['low'] - - return dataframe + strategy = Strategy() + return strategy.populate_indicators(dataframe=dataframe) def populate_buy_trend(dataframe: DataFrame) -> DataFrame: @@ -229,20 +55,8 @@ def populate_buy_trend(dataframe: DataFrame) -> DataFrame: :param dataframe: DataFrame :return: DataFrame with buy column """ - dataframe.loc[ - ( - (dataframe['rsi'] < 35) & - (dataframe['fastd'] < 35) & - (dataframe['adx'] > 30) & - (dataframe['plus_di'] > 0.5) - ) | - ( - (dataframe['adx'] > 65) & - (dataframe['plus_di'] > 0.5) - ), - 'buy'] = 1 - - return dataframe + strategy = Strategy() + return strategy.populate_buy_trend(dataframe=dataframe) def populate_sell_trend(dataframe: DataFrame) -> DataFrame: @@ -251,21 +65,8 @@ def populate_sell_trend(dataframe: DataFrame) -> DataFrame: :param dataframe: DataFrame :return: DataFrame with buy column """ - dataframe.loc[ - ( - ( - (qtpylib.crossed_above(dataframe['rsi'], 70)) | - (qtpylib.crossed_above(dataframe['fastd'], 70)) - ) & - (dataframe['adx'] > 10) & - (dataframe['minus_di'] > 0) - ) | - ( - (dataframe['adx'] > 70) & - (dataframe['minus_di'] > 0.5) - ), - 'sell'] = 1 - return dataframe + strategy = Strategy() + return strategy.populate_sell_trend(dataframe=dataframe) def analyze_ticker(ticker_history: List[Dict]) -> DataFrame: diff --git a/freqtrade/fiat_convert.py b/freqtrade/fiat_convert.py index 0132e531d..91e04fff9 100644 --- a/freqtrade/fiat_convert.py +++ b/freqtrade/fiat_convert.py @@ -48,7 +48,10 @@ class CryptoFiat(): return self._expiration - time.time() <= 0 -class CryptoToFiatConverter(): +class CryptoToFiatConverter(object): + __instance = None + _coinmarketcap = None + # Constants SUPPORTED_FIAT = [ "AUD", "BRL", "CAD", "CHF", "CLP", "CNY", "CZK", "DKK", @@ -57,12 +60,16 @@ class CryptoToFiatConverter(): "RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD" ] - def __init__(self) -> None: - try: - self._coinmarketcap = Pymarketcap() - except BaseException: - self._coinmarketcap = None + def __new__(cls): + if CryptoToFiatConverter.__instance is None: + CryptoToFiatConverter.__instance = object.__new__(cls) + try: + CryptoToFiatConverter._coinmarketcap = Pymarketcap() + except BaseException: + CryptoToFiatConverter._coinmarketcap = None + return CryptoToFiatConverter.__instance + def __init__(self) -> None: self._pairs = [] def convert_amount(self, crypto_amount: float, crypto_symbol: str, fiat_symbol: str) -> float: diff --git a/freqtrade/main.py b/freqtrade/main.py index 27f3dfd9a..a2f44c992 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -19,6 +19,7 @@ from freqtrade.fiat_convert import CryptoToFiatConverter from freqtrade.misc import (State, get_state, load_config, parse_args, throttle, update_state) from freqtrade.persistence import Trade +from freqtrade.strategy.strategy import Strategy logger = logging.getLogger('freqtrade') @@ -191,12 +192,25 @@ def execute_sell(trade: Trade, limit: float) -> None: fmt_exp_profit = round(trade.calc_profit_percent(rate=limit) * 100, 2) profit_trade = trade.calc_profit(rate=limit) + current_rate = exchange.get_ticker(trade.pair, False)['bid'] + profit = trade.calc_profit_percent(current_rate) - message = '*{exchange}:* Selling [{pair}]({pair_url}) with limit `{limit:.8f}`'.format( + message = """*{exchange}:* Selling +*Current Pair:* [{pair}]({pair_url}) +*Limit:* `{limit}` +*Amount:* `{amount}` +*Open Rate:* `{open_rate:.8f}` +*Current Rate:* `{current_rate:.8f}` +*Profit:* `{profit:.2f}%` + """.format( exchange=trade.exchange, - pair=trade.pair.replace('_', '/'), + pair=trade.pair, pair_url=exchange.get_pair_detail_url(trade.pair), - limit=limit + limit=limit, + open_rate=trade.open_rate, + current_rate=current_rate, + amount=round(trade.amount, 8), + profit=round(profit * 100, 2), ) # For regular case, when the configuration exists @@ -235,14 +249,16 @@ def min_roi_reached(trade: Trade, current_rate: float, current_time: datetime) - Based an earlier trade and current price and ROI configuration, decides whether bot should sell :return True if bot should sell at current rate """ + strategy = Strategy() + current_profit = trade.calc_profit_percent(current_rate) - if 'stoploss' in _CONF and current_profit < float(_CONF['stoploss']): + if strategy.stoploss is not None and current_profit < float(strategy.stoploss): logger.debug('Stop loss hit.') return True # Check if time matches and current rate is above threshold time_diff = (current_time - trade.open_date).total_seconds() / 60 - for duration, threshold in sorted(_CONF['minimal_roi'].items()): + for duration, threshold in sorted(strategy.minimal_roi.items()): if time_diff > float(duration) and current_profit > threshold: return True @@ -378,6 +394,9 @@ def init(config: dict, db_url: Optional[str] = None) -> None: persistence.init(config, db_url) exchange.init(config) + strategy = Strategy() + strategy.init(config) + # Set initial application state initial_state = config.get('initial_state') if initial_state: @@ -445,6 +464,9 @@ def main(sysargv=sys.argv[1:]) -> None: # Load and validate configuration _CONF = load_config(args.config) + # Add the strategy file to use + _CONF.update({'strategy': args.strategy}) + # Initialize all modules and start main loop if args.dynamic_whitelist: logger.info('Using dynamically generated whitelist. (--dynamic-whitelist detected)') @@ -462,6 +484,7 @@ def main(sysargv=sys.argv[1:]) -> None: try: init(_CONF) old_state = None + while True: new_state = get_state() # Log state transition @@ -476,7 +499,7 @@ def main(sysargv=sys.argv[1:]) -> None: _process, min_secs=_CONF['internals'].get('process_throttle_secs', 10), nb_assets=args.dynamic_whitelist, - interval=int(_CONF.get('ticker_interval', "5")) + interval=int(_CONF.get('ticker_interval', 5)) ) old_state = new_state except KeyboardInterrupt: diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 9bb287197..7a1631737 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -159,6 +159,14 @@ def common_args_parser(description: str): type=str, metavar='PATH', ) + parser.add_argument( + '-s', '--strategy', + help='specify strategy file (default: freqtrade/strategy/default_strategy.py)', + dest='strategy', + default='.default_strategy', + type=str, + metavar='PATH', + ) return parser @@ -328,7 +336,7 @@ CONF_SCHEMA = { 'type': 'object', 'properties': { 'max_open_trades': {'type': 'integer', 'minimum': 1}, - 'ticker_interval': {'type': 'string', 'enum': ['1', '5', '30', '60', '1440']}, + 'ticker_interval': {'type': 'integer', 'enum': [1, 5, 30, 60, 1440]}, 'stake_currency': {'type': 'string', 'enum': ['BTC', 'ETH', 'USDT']}, 'stake_amount': {'type': 'number', 'minimum': 0.0005}, 'fiat_display_currency': {'type': 'string', 'enum': ['AUD', 'BRL', 'CAD', 'CHF', @@ -419,12 +427,10 @@ CONF_SCHEMA = { ], 'required': [ 'max_open_trades', - 'ticker_interval', 'stake_currency', 'stake_amount', 'fiat_display_currency', 'dry_run', - 'minimal_roi', 'bid_strategy', 'telegram' ] diff --git a/freqtrade/optimize/__init__.py b/freqtrade/optimize/__init__.py index 0ac808424..b68955a6a 100644 --- a/freqtrade/optimize/__init__.py +++ b/freqtrade/optimize/__init__.py @@ -6,9 +6,10 @@ import os from typing import Optional, List, Dict from pandas import DataFrame from freqtrade.exchange import get_ticker_history -from freqtrade.optimize.hyperopt_conf import hyperopt_optimize_conf from freqtrade.analyze import populate_indicators, parse_ticker_dataframe + from freqtrade import misc +from user_data.hyperopt_conf import hyperopt_optimize_conf logger = logging.getLogger(__name__) @@ -127,7 +128,6 @@ def download_backtesting_testdata(datadir: str, pair: str, interval: int = 5) -> pair=filepair, interval=interval, )) - filename = filename.replace('USDT_BTC', 'BTC_FAKEBULL') if os.path.isfile(filename): with open(filename, "rt") as fp: diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 25e1e6231..a21bdb61a 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -14,6 +14,7 @@ from freqtrade.analyze import populate_buy_trend, populate_sell_trend from freqtrade.exchange import Bittrex from freqtrade.main import min_roi_reached from freqtrade.persistence import Trade +from freqtrade.strategy.strategy import Strategy logger = logging.getLogger(__name__) @@ -199,6 +200,11 @@ def start(args): logger.info('Using max_open_trades: %s ...', config['max_open_trades']) max_open_trades = config['max_open_trades'] + # init the strategy to use + config.update({'strategy': args.strategy}) + strategy = Strategy() + strategy.init(config) + # Monkey patch config from freqtrade import main main._CONF = config @@ -216,7 +222,7 @@ def start(args): 'realistic': args.realistic_simulation, 'sell_profit_only': sell_profit_only, 'use_sell_signal': use_sell_signal, - 'stoploss': config.get('stoploss'), + 'stoploss': strategy.stoploss, 'record': args.export }) logger.info( diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index b9780c13a..62dcdf68f 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -3,25 +3,31 @@ import json import logging -import sys +import os import pickle import signal -import os +import sys from functools import reduce from math import exp from operator import itemgetter +from typing import Dict, List +import numpy +import talib.abstract as ta from hyperopt import STATUS_FAIL, STATUS_OK, Trials, fmin, hp, space_eval, tpe from hyperopt.mongoexp import MongoTrials from pandas import DataFrame -from freqtrade import main, misc # noqa -from freqtrade import exchange, optimize +import freqtrade.vendor.qtpylib.indicators as qtpylib +# Monkey patch config +from freqtrade import main # noqa; noqa +from freqtrade import exchange, misc, optimize from freqtrade.exchange import Bittrex from freqtrade.misc import load_config +from freqtrade.optimize import backtesting from freqtrade.optimize.backtesting import backtest -from freqtrade.optimize.hyperopt_conf import hyperopt_optimize_conf -from freqtrade.vendor.qtpylib.indicators import crossed_above +from freqtrade.strategy.strategy import Strategy +from user_data.hyperopt_conf import hyperopt_optimize_conf # Remove noisy log messages logging.getLogger('hyperopt.mongoexp').setLevel(logging.WARNING) @@ -49,69 +55,130 @@ PROCESSED = None # optimize.preprocess(optimize.load_data()) OPTIMIZE_CONFIG = hyperopt_optimize_conf() # Hyperopt Trials -TRIALS_FILE = os.path.join('freqtrade', 'optimize', 'hyperopt_trials.pickle') +TRIALS_FILE = os.path.join('user_data', 'hyperopt_trials.pickle') TRIALS = Trials() -# Monkey patch config -from freqtrade import main # noqa main._CONF = OPTIMIZE_CONFIG -SPACE = { - 'macd_below_zero': hp.choice('macd_below_zero', [ - {'enabled': False}, - {'enabled': True} - ]), - 'mfi': hp.choice('mfi', [ - {'enabled': False}, - {'enabled': True, 'value': hp.quniform('mfi-value', 5, 25, 1)} - ]), - 'fastd': hp.choice('fastd', [ - {'enabled': False}, - {'enabled': True, 'value': hp.quniform('fastd-value', 10, 50, 1)} - ]), - 'adx': hp.choice('adx', [ - {'enabled': False}, - {'enabled': True, 'value': hp.quniform('adx-value', 15, 50, 1)} - ]), - 'rsi': hp.choice('rsi', [ - {'enabled': False}, - {'enabled': True, 'value': hp.quniform('rsi-value', 20, 40, 1)} - ]), - 'uptrend_long_ema': hp.choice('uptrend_long_ema', [ - {'enabled': False}, - {'enabled': True} - ]), - 'uptrend_short_ema': hp.choice('uptrend_short_ema', [ - {'enabled': False}, - {'enabled': True} - ]), - 'over_sar': hp.choice('over_sar', [ - {'enabled': False}, - {'enabled': True} - ]), - 'green_candle': hp.choice('green_candle', [ - {'enabled': False}, - {'enabled': True} - ]), - 'uptrend_sma': hp.choice('uptrend_sma', [ - {'enabled': False}, - {'enabled': True} - ]), - 'trigger': hp.choice('trigger', [ - {'type': 'lower_bb'}, - {'type': 'lower_bb_tema'}, - {'type': 'faststoch10'}, - {'type': 'ao_cross_zero'}, - {'type': 'ema3_cross_ema10'}, - {'type': 'macd_cross_signal'}, - {'type': 'sar_reversal'}, - {'type': 'ht_sine'}, - {'type': 'heiken_reversal_bull'}, - {'type': 'di_cross'}, - ]), - 'stoploss': hp.uniform('stoploss', -0.5, -0.02), -} +def populate_indicators(dataframe: DataFrame) -> DataFrame: + """ + Adds several different TA indicators to the given DataFrame + """ + dataframe['adx'] = ta.ADX(dataframe) + dataframe['ao'] = qtpylib.awesome_oscillator(dataframe) + dataframe['cci'] = ta.CCI(dataframe) + macd = ta.MACD(dataframe) + dataframe['macd'] = macd['macd'] + dataframe['macdsignal'] = macd['macdsignal'] + dataframe['macdhist'] = macd['macdhist'] + dataframe['mfi'] = ta.MFI(dataframe) + dataframe['minus_dm'] = ta.MINUS_DM(dataframe) + dataframe['minus_di'] = ta.MINUS_DI(dataframe) + dataframe['plus_dm'] = ta.PLUS_DM(dataframe) + dataframe['plus_di'] = ta.PLUS_DI(dataframe) + dataframe['roc'] = ta.ROC(dataframe) + 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) + # Inverse Fisher transform on RSI normalized, value [0.0, 100.0] (https://goo.gl/2JGGoy) + dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1) + # Stoch + stoch = ta.STOCH(dataframe) + dataframe['slowd'] = stoch['slowd'] + dataframe['slowk'] = stoch['slowk'] + # Stoch fast + stoch_fast = ta.STOCHF(dataframe) + dataframe['fastd'] = stoch_fast['fastd'] + dataframe['fastk'] = stoch_fast['fastk'] + # Stoch RSI + stoch_rsi = ta.STOCHRSI(dataframe) + dataframe['fastd_rsi'] = stoch_rsi['fastd'] + dataframe['fastk_rsi'] = stoch_rsi['fastk'] + # Bollinger bands + bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) + dataframe['bb_lowerband'] = bollinger['lower'] + dataframe['bb_middleband'] = bollinger['mid'] + dataframe['bb_upperband'] = bollinger['upper'] + # EMA - Exponential Moving Average + dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3) + dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5) + dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10) + dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50) + dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100) + # SAR Parabolic + dataframe['sar'] = ta.SAR(dataframe) + # SMA - Simple Moving Average + dataframe['sma'] = ta.SMA(dataframe, timeperiod=40) + # TEMA - Triple Exponential Moving Average + dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9) + # Hilbert Transform Indicator - SineWave + hilbert = ta.HT_SINE(dataframe) + dataframe['htsine'] = hilbert['sine'] + dataframe['htleadsine'] = hilbert['leadsine'] + + # Pattern Recognition - Bullish candlestick patterns + # ------------------------------------ + """ + # Hammer: values [0, 100] + dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe) + # Inverted Hammer: values [0, 100] + dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe) + # Dragonfly Doji: values [0, 100] + dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe) + # Piercing Line: values [0, 100] + dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100] + # Morningstar: values [0, 100] + dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100] + # Three White Soldiers: values [0, 100] + dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100] + """ + + # Pattern Recognition - Bearish candlestick patterns + # ------------------------------------ + """ + # Hanging Man: values [0, 100] + dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe) + # Shooting Star: values [0, 100] + dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe) + # Gravestone Doji: values [0, 100] + dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe) + # Dark Cloud Cover: values [0, 100] + dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe) + # Evening Doji Star: values [0, 100] + dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe) + # Evening Star: values [0, 100] + dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe) + """ + + # Pattern Recognition - Bullish/Bearish candlestick patterns + # ------------------------------------ + """ + # Three Line Strike: values [0, -100, 100] + dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe) + # Spinning Top: values [0, -100, 100] + dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100] + # Engulfing: values [0, -100, 100] + dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100] + # Harami: values [0, -100, 100] + dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100] + # Three Outside Up/Down: values [0, -100, 100] + dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100] + # Three Inside Up/Down: values [0, -100, 100] + dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100] + """ + + # Chart type + # ------------------------------------ + # Heikinashi stategy + heikinashi = qtpylib.heikinashi(dataframe) + dataframe['ha_open'] = heikinashi['open'] + dataframe['ha_close'] = heikinashi['close'] + dataframe['ha_high'] = heikinashi['high'] + dataframe['ha_low'] = heikinashi['low'] + + return dataframe def save_trials(trials, trials_path=TRIALS_FILE): @@ -158,10 +225,145 @@ def calculate_loss(total_profit: float, trade_count: int, trade_duration: float) return trade_loss + profit_loss + duration_loss +def hyperopt_space() -> List[Dict]: + """ + Define your Hyperopt space for searching strategy parameters + """ + space = { + 'macd_below_zero': hp.choice('macd_below_zero', [ + {'enabled': False}, + {'enabled': True} + ]), + 'mfi': hp.choice('mfi', [ + {'enabled': False}, + {'enabled': True, 'value': hp.quniform('mfi-value', 5, 25, 1)} + ]), + 'fastd': hp.choice('fastd', [ + {'enabled': False}, + {'enabled': True, 'value': hp.quniform('fastd-value', 10, 50, 1)} + ]), + 'adx': hp.choice('adx', [ + {'enabled': False}, + {'enabled': True, 'value': hp.quniform('adx-value', 15, 50, 1)} + ]), + 'rsi': hp.choice('rsi', [ + {'enabled': False}, + {'enabled': True, 'value': hp.quniform('rsi-value', 20, 40, 1)} + ]), + 'uptrend_long_ema': hp.choice('uptrend_long_ema', [ + {'enabled': False}, + {'enabled': True} + ]), + 'uptrend_short_ema': hp.choice('uptrend_short_ema', [ + {'enabled': False}, + {'enabled': True} + ]), + 'over_sar': hp.choice('over_sar', [ + {'enabled': False}, + {'enabled': True} + ]), + 'green_candle': hp.choice('green_candle', [ + {'enabled': False}, + {'enabled': True} + ]), + 'uptrend_sma': hp.choice('uptrend_sma', [ + {'enabled': False}, + {'enabled': True} + ]), + 'trigger': hp.choice('trigger', [ + {'type': 'lower_bb'}, + {'type': 'lower_bb_tema'}, + {'type': 'faststoch10'}, + {'type': 'ao_cross_zero'}, + {'type': 'ema3_cross_ema10'}, + {'type': 'macd_cross_signal'}, + {'type': 'sar_reversal'}, + {'type': 'ht_sine'}, + {'type': 'heiken_reversal_bull'}, + {'type': 'di_cross'}, + ]), + 'stoploss': hp.uniform('stoploss', -0.5, -0.02), + } + return space + + +def buy_strategy_generator(params) -> None: + """ + Define the buy strategy parameters to be used by hyperopt + """ + def populate_buy_trend(dataframe: DataFrame) -> DataFrame: + conditions = [] + # GUARDS AND TRENDS + if 'uptrend_long_ema' in params and params['uptrend_long_ema']['enabled']: + conditions.append(dataframe['ema50'] > dataframe['ema100']) + if 'macd_below_zero' in params and params['macd_below_zero']['enabled']: + conditions.append(dataframe['macd'] < 0) + if 'uptrend_short_ema' in params and params['uptrend_short_ema']['enabled']: + conditions.append(dataframe['ema5'] > dataframe['ema10']) + if 'mfi' in params and params['mfi']['enabled']: + conditions.append(dataframe['mfi'] < params['mfi']['value']) + if 'fastd' in params and params['fastd']['enabled']: + conditions.append(dataframe['fastd'] < params['fastd']['value']) + if 'adx' in params and params['adx']['enabled']: + conditions.append(dataframe['adx'] > params['adx']['value']) + if 'rsi' in params and params['rsi']['enabled']: + conditions.append(dataframe['rsi'] < params['rsi']['value']) + if 'over_sar' in params and params['over_sar']['enabled']: + conditions.append(dataframe['close'] > dataframe['sar']) + if 'green_candle' in params and params['green_candle']['enabled']: + conditions.append(dataframe['close'] > dataframe['open']) + if 'uptrend_sma' in params and params['uptrend_sma']['enabled']: + prevsma = dataframe['sma'].shift(1) + conditions.append(dataframe['sma'] > prevsma) + + # TRIGGERS + triggers = { + 'lower_bb': ( + dataframe['close'] < dataframe['bb_lowerband'] + ), + 'lower_bb_tema': ( + dataframe['tema'] < dataframe['bb_lowerband'] + ), + 'faststoch10': (qtpylib.crossed_above( + dataframe['fastd'], 10.0 + )), + 'ao_cross_zero': (qtpylib.crossed_above( + dataframe['ao'], 0.0 + )), + 'ema3_cross_ema10': (qtpylib.crossed_above( + dataframe['ema3'], dataframe['ema10'] + )), + 'macd_cross_signal': (qtpylib.crossed_above( + dataframe['macd'], dataframe['macdsignal'] + )), + 'sar_reversal': (qtpylib.crossed_above( + dataframe['close'], dataframe['sar'] + )), + 'ht_sine': (qtpylib.crossed_above( + dataframe['htleadsine'], dataframe['htsine'] + )), + 'heiken_reversal_bull': ( + (qtpylib.crossed_above(dataframe['ha_close'], dataframe['ha_open'])) & + (dataframe['ha_low'] == dataframe['ha_open']) + ), + 'di_cross': (qtpylib.crossed_above( + dataframe['plus_di'], dataframe['minus_di'] + )), + } + conditions.append(triggers.get(params['trigger']['type'])) + + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'buy'] = 1 + + return dataframe + + return populate_buy_trend + + def optimizer(params): global _CURRENT_TRIES - from freqtrade.optimize import backtesting backtesting.populate_buy_trend = buy_strategy_generator(params) results = backtest({'stake_amount': OPTIMIZE_CONFIG['stake_amount'], @@ -209,58 +411,8 @@ def format_results(results: DataFrame): ) -def buy_strategy_generator(params): - def populate_buy_trend(dataframe: DataFrame) -> DataFrame: - conditions = [] - # GUARDS AND TRENDS - if params['uptrend_long_ema']['enabled']: - conditions.append(dataframe['ema50'] > dataframe['ema100']) - if params['macd_below_zero']['enabled']: - conditions.append(dataframe['macd'] < 0) - if params['uptrend_short_ema']['enabled']: - conditions.append(dataframe['ema5'] > dataframe['ema10']) - if params['mfi']['enabled']: - conditions.append(dataframe['mfi'] < params['mfi']['value']) - if params['fastd']['enabled']: - conditions.append(dataframe['fastd'] < params['fastd']['value']) - if params['adx']['enabled']: - conditions.append(dataframe['adx'] > params['adx']['value']) - if params['rsi']['enabled']: - conditions.append(dataframe['rsi'] < params['rsi']['value']) - if params['over_sar']['enabled']: - conditions.append(dataframe['close'] > dataframe['sar']) - if params['green_candle']['enabled']: - conditions.append(dataframe['close'] > dataframe['open']) - if params['uptrend_sma']['enabled']: - prevsma = dataframe['sma'].shift(1) - conditions.append(dataframe['sma'] > prevsma) - - # TRIGGERS - triggers = { - 'lower_bb': (dataframe['close'] < dataframe['bb_lowerband']), - 'lower_bb_tema': (dataframe['tema'] < dataframe['bb_lowerband']), - 'faststoch10': (crossed_above(dataframe['fastd'], 10.0)), - 'ao_cross_zero': (crossed_above(dataframe['ao'], 0.0)), - 'ema3_cross_ema10': (crossed_above(dataframe['ema3'], dataframe['ema10'])), - 'macd_cross_signal': (crossed_above(dataframe['macd'], dataframe['macdsignal'])), - 'sar_reversal': (crossed_above(dataframe['close'], dataframe['sar'])), - 'ht_sine': (crossed_above(dataframe['htleadsine'], dataframe['htsine'])), - 'heiken_reversal_bull': (crossed_above(dataframe['ha_close'], dataframe['ha_open'])) & - (dataframe['ha_low'] == dataframe['ha_open']), - 'di_cross': (crossed_above(dataframe['plus_di'], dataframe['minus_di'])), - } - conditions.append(triggers.get(params['trigger']['type'])) - - dataframe.loc[ - reduce(lambda x, y: x & y, conditions), - 'buy'] = 1 - - return dataframe - return populate_buy_trend - - def start(args): - global TOTAL_TRIES, PROCESSED, SPACE, TRIALS, _CURRENT_TRIES + global TOTAL_TRIES, PROCESSED, TRIALS, _CURRENT_TRIES TOTAL_TRIES = args.epochs @@ -275,10 +427,17 @@ def start(args): logger.info('Using config: %s ...', args.config) config = load_config(args.config) pairs = config['exchange']['pair_whitelist'] + + # init the strategy to use + config.update({'strategy': args.strategy}) + strategy = Strategy() + strategy.init(config) + timerange = misc.parse_timerange(args.timerange) data = optimize.load_data(args.datadir, pairs=pairs, ticker_interval=args.ticker_interval, timerange=timerange) + optimize.populate_indicators = populate_indicators PROCESSED = optimize.tickerdata_to_dataframe(data) if args.mongodb: @@ -303,7 +462,7 @@ def start(args): try: best_parameters = fmin( fn=optimizer, - space=SPACE, + space=hyperopt_space(), algo=tpe.suggest, max_evals=TOTAL_TRIES, trials=TRIALS @@ -319,7 +478,10 @@ def start(args): # Improve best parameter logging display if best_parameters: - best_parameters = space_eval(SPACE, best_parameters) + best_parameters = space_eval( + hyperopt_space(), + best_parameters + ) logger.info('Best parameters:\n%s', json.dumps(best_parameters, indent=4)) logger.info('Best Result:\n%s', best_result) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 0cac3bbe9..79903d66f 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -47,6 +47,10 @@ def init(config: dict, engine: Optional[Engine] = None) -> None: Trade.query = session.query_property() _DECL_BASE.metadata.create_all(engine) + # Clean dry_run DB + if _CONF.get('dry_run', False) and _CONF.get('dry_run_db', False): + clean_dry_run_db() + def cleanup() -> None: """ @@ -56,6 +60,17 @@ def cleanup() -> None: Trade.session.flush() +def clean_dry_run_db() -> None: + """ + Remove open_order_id from a Dry_run DB + :return: None + """ + for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all(): + # Check we are updating only a dry_run order not a prod one + if 'dry_run' in trade.open_order_id: + trade.open_order_id = None + + class Trade(_DECL_BASE): __tablename__ = 'trades' diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 0fdc734f4..70d5a78f3 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -147,7 +147,7 @@ def _status(bot: Bot, update: Update) -> None: ) if trade.close_profit else None message = """ *Trade ID:* `{trade_id}` -*Current Pair:* [{pair}]({market_url}) +*Current Pair:* [{pair}]({pair_url}) *Open Since:* `{date}` *Amount:* `{amount}` *Open Rate:* `{open_rate:.8f}` @@ -156,10 +156,11 @@ def _status(bot: Bot, update: Update) -> None: *Close Profit:* `{close_profit}` *Current Profit:* `{current_profit:.2f}%` *Open Order:* `{open_order}` +*Total Open Trades:* `{total_trades}` """.format( trade_id=trade.id, pair=trade.pair, - market_url=exchange.get_pair_detail_url(trade.pair), + pair_url=exchange.get_pair_detail_url(trade.pair), date=arrow.get(trade.open_date).humanize(), open_rate=trade.open_rate, close_rate=trade.close_rate, @@ -170,6 +171,7 @@ def _status(bot: Bot, update: Update) -> None: open_order='({} rem={:.8f})'.format( order['type'], order['remaining'] ) if order else None, + total_trades=len(trades) ) send_msg(message, bot=bot) diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/freqtrade/strategy/default_strategy.py b/freqtrade/strategy/default_strategy.py new file mode 100644 index 000000000..c89b20527 --- /dev/null +++ b/freqtrade/strategy/default_strategy.py @@ -0,0 +1,238 @@ +import talib.abstract as ta +import freqtrade.vendor.qtpylib.indicators as qtpylib +from freqtrade.strategy.interface import IStrategy +from pandas import DataFrame + + +class_name = 'DefaultStrategy' + + +class DefaultStrategy(IStrategy): + """ + Default Strategy provided by freqtrade bot. + You can override it with your own strategy + """ + + # Minimal ROI designed for the strategy + minimal_roi = { + "40": 0.0, + "30": 0.01, + "20": 0.02, + "0": 0.04 + } + + # Optimal stoploss designed for the strategy + stoploss = -0.10 + + # Optimal ticker interval for the strategy + ticker_interval = 5 + + def populate_indicators(self, dataframe: DataFrame) -> 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. + """ + + # Momentum Indicator + # ------------------------------------ + + # ADX + dataframe['adx'] = ta.ADX(dataframe) + + # Awesome oscillator + dataframe['ao'] = qtpylib.awesome_oscillator(dataframe) + """ + # Commodity Channel Index: values Oversold:<-100, Overbought:>100 + dataframe['cci'] = ta.CCI(dataframe) + """ + # MACD + macd = ta.MACD(dataframe) + dataframe['macd'] = macd['macd'] + dataframe['macdsignal'] = macd['macdsignal'] + dataframe['macdhist'] = macd['macdhist'] + + # MFI + dataframe['mfi'] = ta.MFI(dataframe) + + # Minus Directional Indicator / Movement + dataframe['minus_dm'] = ta.MINUS_DM(dataframe) + dataframe['minus_di'] = ta.MINUS_DI(dataframe) + + # Plus Directional Indicator / Movement + dataframe['plus_dm'] = ta.PLUS_DM(dataframe) + dataframe['plus_di'] = ta.PLUS_DI(dataframe) + dataframe['minus_di'] = ta.MINUS_DI(dataframe) + + """ + # ROC + dataframe['roc'] = ta.ROC(dataframe) + """ + # 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) + # Inverse Fisher transform on RSI normalized, value [0.0, 100.0] (https://goo.gl/2JGGoy) + dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1) + # Stoch + stoch = ta.STOCH(dataframe) + dataframe['slowd'] = stoch['slowd'] + dataframe['slowk'] = stoch['slowk'] + """ + # Stoch fast + stoch_fast = ta.STOCHF(dataframe) + dataframe['fastd'] = stoch_fast['fastd'] + dataframe['fastk'] = stoch_fast['fastk'] + """ + # Stoch RSI + stoch_rsi = ta.STOCHRSI(dataframe) + dataframe['fastd_rsi'] = stoch_rsi['fastd'] + dataframe['fastk_rsi'] = stoch_rsi['fastk'] + """ + + # Overlap Studies + # ------------------------------------ + + # Previous Bollinger bands + # Because ta.BBANDS implementation is broken with small numbers, it actually + # returns middle band for all the three bands. Switch to qtpylib.bollinger_bands + # and use middle band instead. + dataframe['blower'] = ta.BBANDS(dataframe, nbdevup=2, nbdevdn=2)['lowerband'] + + # Bollinger bands + bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) + dataframe['bb_lowerband'] = bollinger['lower'] + dataframe['bb_middleband'] = bollinger['mid'] + dataframe['bb_upperband'] = bollinger['upper'] + + # EMA - Exponential Moving Average + dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3) + dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5) + dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10) + dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50) + dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100) + + # SAR Parabol + dataframe['sar'] = ta.SAR(dataframe) + + # SMA - Simple Moving Average + dataframe['sma'] = ta.SMA(dataframe, timeperiod=40) + + # TEMA - Triple Exponential Moving Average + dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9) + + # Cycle Indicator + # ------------------------------------ + # Hilbert Transform Indicator - SineWave + hilbert = ta.HT_SINE(dataframe) + dataframe['htsine'] = hilbert['sine'] + dataframe['htleadsine'] = hilbert['leadsine'] + + # Pattern Recognition - Bullish candlestick patterns + # ------------------------------------ + """ + # Hammer: values [0, 100] + dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe) + # Inverted Hammer: values [0, 100] + dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe) + # Dragonfly Doji: values [0, 100] + dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe) + # Piercing Line: values [0, 100] + dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100] + # Morningstar: values [0, 100] + dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100] + # Three White Soldiers: values [0, 100] + dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100] + """ + + # Pattern Recognition - Bearish candlestick patterns + # ------------------------------------ + """ + # Hanging Man: values [0, 100] + dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe) + # Shooting Star: values [0, 100] + dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe) + # Gravestone Doji: values [0, 100] + dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe) + # Dark Cloud Cover: values [0, 100] + dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe) + # Evening Doji Star: values [0, 100] + dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe) + # Evening Star: values [0, 100] + dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe) + """ + + # Pattern Recognition - Bullish/Bearish candlestick patterns + # ------------------------------------ + """ + # Three Line Strike: values [0, -100, 100] + dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe) + # Spinning Top: values [0, -100, 100] + dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100] + # Engulfing: values [0, -100, 100] + dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100] + # Harami: values [0, -100, 100] + dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100] + # Three Outside Up/Down: values [0, -100, 100] + dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100] + # Three Inside Up/Down: values [0, -100, 100] + dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100] + """ + + # Chart type + # ------------------------------------ + # Heikinashi stategy + heikinashi = qtpylib.heikinashi(dataframe) + dataframe['ha_open'] = heikinashi['open'] + dataframe['ha_close'] = heikinashi['close'] + dataframe['ha_high'] = heikinashi['high'] + dataframe['ha_low'] = heikinashi['low'] + + return dataframe + + def populate_buy_trend(self, dataframe: DataFrame) -> 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'] < 35) & + (dataframe['fastd'] < 35) & + (dataframe['adx'] > 30) & + (dataframe['plus_di'] > 0.5) + ) | + ( + (dataframe['adx'] > 65) & + (dataframe['plus_di'] > 0.5) + ), + 'buy'] = 1 + + return dataframe + + def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame: + """ + Based on TA indicators, populates the sell signal for the given dataframe + :param dataframe: DataFrame + :return: DataFrame with buy column + """ + dataframe.loc[ + ( + ( + (qtpylib.crossed_above(dataframe['rsi'], 70)) | + (qtpylib.crossed_above(dataframe['fastd'], 70)) + ) & + (dataframe['adx'] > 10) & + (dataframe['minus_di'] > 0) + ) | + ( + (dataframe['adx'] > 70) & + (dataframe['minus_di'] > 0.5) + ), + 'sell'] = 1 + return dataframe diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py new file mode 100644 index 000000000..ce5f08cd2 --- /dev/null +++ b/freqtrade/strategy/interface.py @@ -0,0 +1,44 @@ +from abc import ABC, abstractmethod +from pandas import DataFrame + + +class IStrategy(ABC): + @property + def name(self) -> str: + """ + Name of the strategy. + :return: str representation of the class name + """ + return self.__class__.__name__ + + """ + Attributes you can use: + minimal_roi -> Dict: Minimal ROI designed for the strategy + stoploss -> float: optimal stoploss designed for the strategy + ticker_interval -> int: value of the ticker interval to use for the strategy + """ + + @abstractmethod + def populate_indicators(self, dataframe: DataFrame) -> DataFrame: + """ + Populate indicators that will be used in the Buy and Sell strategy + :param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe() + :return: a Dataframe with all mandatory indicators for the strategies + """ + + @abstractmethod + def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: + """ + Based on TA indicators, populates the buy signal for the given dataframe + :param dataframe: DataFrame + :return: DataFrame with buy column + :return: + """ + + @abstractmethod + def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame: + """ + Based on TA indicators, populates the sell signal for the given dataframe + :param dataframe: DataFrame + :return: DataFrame with buy column + """ diff --git a/freqtrade/strategy/strategy.py b/freqtrade/strategy/strategy.py new file mode 100644 index 000000000..2545e378c --- /dev/null +++ b/freqtrade/strategy/strategy.py @@ -0,0 +1,166 @@ +import os +import sys +import logging +import importlib + +from pandas import DataFrame +from typing import Dict +from freqtrade.strategy.interface import IStrategy + + +sys.path.insert(0, r'../../user_data/strategies') + + +class Strategy(object): + __instance = None + + DEFAULT_STRATEGY = 'default_strategy' + + def __new__(cls): + if Strategy.__instance is None: + Strategy.__instance = object.__new__(cls) + return Strategy.__instance + + def init(self, config): + self.logger = logging.getLogger(__name__) + + # Verify the strategy is in the configuration, otherwise fallback to the default strategy + if 'strategy' in config: + strategy = config['strategy'] + else: + strategy = self.DEFAULT_STRATEGY + + # Load the strategy + self._load_strategy(strategy) + + # Set attributes + # Check if we need to override configuration + if 'minimal_roi' in config: + self.custom_strategy.minimal_roi = config['minimal_roi'] + self.logger.info("Override strategy \'minimal_roi\' with value in config file.") + + if 'stoploss' in config: + self.custom_strategy.stoploss = config['stoploss'] + self.logger.info( + "Override strategy \'stoploss\' with value in config file: {}.".format( + config['stoploss'] + ) + ) + + if 'ticker_interval' in config: + self.custom_strategy.ticker_interval = config['ticker_interval'] + self.logger.info( + "Override strategy \'ticker_interval\' with value in config file: {}.".format( + config['ticker_interval'] + ) + ) + + self.minimal_roi = self.custom_strategy.minimal_roi + self.stoploss = self.custom_strategy.stoploss + self.ticker_interval = self.custom_strategy.ticker_interval + + def _load_strategy(self, strategy_name: str) -> None: + """ + Search and load the custom strategy. If no strategy found, fallback on the default strategy + Set the object into self.custom_strategy + :param strategy_name: name of the module to import + :return: None + """ + + try: + # Start by sanitizing the file name (remove any extensions) + strategy_name = self._sanitize_module_name(filename=strategy_name) + + # Search where can be the strategy file + path = self._search_strategy(filename=strategy_name) + + # Load the strategy + self.custom_strategy = self._load_class(path + strategy_name) + + # Fallback to the default strategy + except (ImportError, TypeError): + self.custom_strategy = self._load_class('.' + self.DEFAULT_STRATEGY) + + def _load_class(self, filename: str) -> IStrategy: + """ + Import a strategy as a module + :param filename: path to the strategy (path from freqtrade/strategy/) + :return: return the strategy class + """ + module = importlib.import_module(filename, __package__) + custom_strategy = getattr(module, module.class_name) + + self.logger.info("Load strategy class: {} ({}.py)".format(module.class_name, filename)) + return custom_strategy() + + @staticmethod + def _sanitize_module_name(filename: str) -> str: + """ + Remove any extension from filename + :param filename: filename to sanatize + :return: return the filename without extensions + """ + filename = os.path.basename(filename) + filename = os.path.splitext(filename)[0] + return filename + + @staticmethod + def _search_strategy(filename: str) -> str: + """ + Search for the Strategy file in different folder + 1. search into the user_data/strategies folder + 2. search into the freqtrade/strategy folder + 3. if nothing found, return None + :param strategy_name: module name to search + :return: module path where is the strategy + """ + pwd = os.path.dirname(os.path.realpath(__file__)) + '/' + user_data = os.path.join(pwd, '..', '..', 'user_data', 'strategies', filename + '.py') + strategy_folder = os.path.join(pwd, filename + '.py') + + path = None + if os.path.isfile(user_data): + path = 'user_data.strategies.' + elif os.path.isfile(strategy_folder): + path = '.' + + return path + + def minimal_roi(self) -> Dict: + """ + Minimal ROI designed for the strategy + :return: Dict: Value for the Minimal ROI + """ + return + + def stoploss(self) -> float: + """ + Optimal stoploss designed for the strategy + :return: float | return None to disable it + """ + return self.custom_strategy.stoploss + + def populate_indicators(self, dataframe: DataFrame) -> DataFrame: + """ + Populate indicators that will be used in the Buy and Sell strategy + :param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe() + :return: a Dataframe with all mandatory indicators for the strategies + """ + return self.custom_strategy.populate_indicators(dataframe) + + def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: + """ + Based on TA indicators, populates the buy signal for the given dataframe + :param dataframe: DataFrame + :return: DataFrame with buy column + :return: + """ + return self.custom_strategy.populate_buy_trend(dataframe) + + def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame: + """ + Based on TA indicators, populates the sell signal for the given dataframe + :param dataframe: DataFrame + :return: DataFrame with buy column + """ + return self.custom_strategy.populate_sell_trend(dataframe) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 37dc3e894..70249fd7d 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -18,7 +18,7 @@ def default_conf(): "stake_currency": "BTC", "stake_amount": 0.001, "fiat_display_currency": "USD", - "ticker_interval": "5", + "ticker_interval": 5, "dry_run": True, "minimal_roi": { "40": 0.0, @@ -217,3 +217,33 @@ def ticker_history(): "BV": 0.7039405 } ] + + +@pytest.fixture +def ticker_history_without_bv(): + return [ + { + "O": 8.794e-05, + "H": 8.948e-05, + "L": 8.794e-05, + "C": 8.88e-05, + "V": 991.09056638, + "T": "2017-11-26T08:50:00" + }, + { + "O": 8.88e-05, + "H": 8.942e-05, + "L": 8.88e-05, + "C": 8.893e-05, + "V": 658.77935965, + "T": "2017-11-26T08:55:00" + }, + { + "O": 8.891e-05, + "H": 8.893e-05, + "L": 8.875e-05, + "C": 8.877e-05, + "V": 7920.73570705, + "T": "2017-11-26T09:00:00" + } + ] diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 2872df83f..f88cdd9b9 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -3,12 +3,21 @@ import logging import math import pandas as pd +import pytest from unittest.mock import MagicMock from freqtrade import exchange, optimize from freqtrade.exchange import Bittrex from freqtrade.optimize import preprocess from freqtrade.optimize.backtesting import backtest, generate_text_table, get_timeframe import freqtrade.optimize.backtesting as backtesting +from freqtrade.strategy.strategy import Strategy + + +@pytest.fixture +def default_strategy(): + strategy = Strategy() + strategy.init({'strategy': 'default_strategy'}) + return strategy def trim_dictlist(dl, num): @@ -37,7 +46,7 @@ def test_generate_text_table(): 'TOTAL 2 15.00 0.60000000 100.0 2 0') # noqa -def test_get_timeframe(): +def test_get_timeframe(default_strategy): data = preprocess(optimize.load_data( None, ticker_interval=1, pairs=['BTC_UNITEST'])) min_date, max_date = get_timeframe(data) @@ -45,7 +54,7 @@ def test_get_timeframe(): assert max_date.isoformat() == '2017-11-14T22:59:00+00:00' -def test_backtest(default_conf, mocker): +def test_backtest(default_strategy, default_conf, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) exchange._API = Bittrex({'key': '', 'secret': ''}) @@ -58,7 +67,7 @@ def test_backtest(default_conf, mocker): assert not results.empty -def test_backtest_1min_ticker_interval(default_conf, mocker): +def test_backtest_1min_ticker_interval(default_strategy, default_conf, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) exchange._API = Bittrex({'key': '', 'secret': ''}) @@ -131,7 +140,7 @@ def simple_backtest(config, contour, num_results): # loaded by freqdata/optimize/__init__.py::load_data() -def test_backtest2(default_conf, mocker): +def test_backtest2(default_conf, mocker, default_strategy): mocker.patch.dict('freqtrade.main._CONF', default_conf) data = optimize.load_data(None, ticker_interval=5, pairs=['BTC_ETH']) data = trim_dictlist(data, -200) @@ -142,7 +151,7 @@ def test_backtest2(default_conf, mocker): assert not results.empty -def test_processed(default_conf, mocker): +def test_processed(default_conf, mocker, default_strategy): mocker.patch.dict('freqtrade.main._CONF', default_conf) dict_of_tickerrows = load_data_test('raise') dataframes = optimize.preprocess(dict_of_tickerrows) @@ -154,7 +163,7 @@ def test_processed(default_conf, mocker): assert col in cols -def test_backtest_pricecontours(default_conf, mocker): +def test_backtest_pricecontours(default_conf, mocker, default_strategy): mocker.patch.dict('freqtrade.main._CONF', default_conf) tests = [['raise', 17], ['lower', 0], ['sine', 17]] for [contour, numres] in tests: diff --git a/freqtrade/tests/optimize/test_hyperopt_config.py b/freqtrade/tests/optimize/test_hyperopt_config.py index e06c1f2eb..aa9424826 100644 --- a/freqtrade/tests/optimize/test_hyperopt_config.py +++ b/freqtrade/tests/optimize/test_hyperopt_config.py @@ -1,6 +1,6 @@ # pragma pylint: disable=missing-docstring,W0212 -from freqtrade.optimize.hyperopt_conf import hyperopt_optimize_conf +from user_data.hyperopt_conf import hyperopt_optimize_conf def test_hyperopt_optimize_conf(): diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index 9c99be342..986f3f8f0 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -219,9 +219,7 @@ def test_forcesell_handle(default_conf, update, ticker, ticker_sell_up, mocker): mocker.patch.multiple('freqtrade.main.exchange', validate_pairs=MagicMock(), get_ticker=ticker) - mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap', - ticker=MagicMock(return_value={'price_usd': 15000.0}), - _cache_symbols=MagicMock(return_value={'BTC': 1})) + mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) init(default_conf, create_engine('sqlite://')) # Create some test data @@ -239,7 +237,9 @@ def test_forcesell_handle(default_conf, update, ticker, ticker_sell_up, mocker): _forcesell(bot=MagicMock(), update=update) assert rpc_mock.call_count == 2 - assert 'Selling [BTC/ETH]' in rpc_mock.call_args_list[-1][0][0] + assert 'Selling' in rpc_mock.call_args_list[-1][0][0] + assert '[BTC_ETH]' in rpc_mock.call_args_list[-1][0][0] + assert 'Amount' in rpc_mock.call_args_list[-1][0][0] assert '0.00001172' in rpc_mock.call_args_list[-1][0][0] assert 'profit: 6.11%, 0.00006126' in rpc_mock.call_args_list[-1][0][0] assert '0.919 USD' in rpc_mock.call_args_list[-1][0][0] @@ -256,9 +256,7 @@ def test_forcesell_down_handle(default_conf, update, ticker, ticker_sell_down, m mocker.patch.multiple('freqtrade.main.exchange', validate_pairs=MagicMock(), get_ticker=ticker) - mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap', - ticker=MagicMock(return_value={'price_usd': 15000.0}), - _cache_symbols=MagicMock(return_value={'BTC': 1})) + mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) init(default_conf, create_engine('sqlite://')) # Create some test data @@ -276,7 +274,9 @@ def test_forcesell_down_handle(default_conf, update, ticker, ticker_sell_down, m _forcesell(bot=MagicMock(), update=update) assert rpc_mock.call_count == 2 - assert 'Selling [BTC/ETH]' in rpc_mock.call_args_list[-1][0][0] + assert 'Selling' in rpc_mock.call_args_list[-1][0][0] + assert '[BTC_ETH]' in rpc_mock.call_args_list[-1][0][0] + assert 'Amount' in rpc_mock.call_args_list[-1][0][0] assert '0.00001044' in rpc_mock.call_args_list[-1][0][0] assert 'loss: -5.48%, -0.00005492' in rpc_mock.call_args_list[-1][0][0] assert '-0.824 USD' in rpc_mock.call_args_list[-1][0][0] @@ -317,9 +317,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, mocker): mocker.patch.multiple('freqtrade.main.exchange', validate_pairs=MagicMock(), get_ticker=ticker) - mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap', - ticker=MagicMock(return_value={'price_usd': 15000.0}), - _cache_symbols=MagicMock(return_value={'BTC': 1})) + mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) init(default_conf, create_engine('sqlite://')) # Create some test data diff --git a/freqtrade/tests/strategy/test_default_strategy.py b/freqtrade/tests/strategy/test_default_strategy.py new file mode 100644 index 000000000..f23c1fa48 --- /dev/null +++ b/freqtrade/tests/strategy/test_default_strategy.py @@ -0,0 +1,36 @@ +import json +import pytest +from pandas import DataFrame +from freqtrade.strategy.default_strategy import DefaultStrategy, class_name +from freqtrade.analyze import parse_ticker_dataframe + + +@pytest.fixture +def result(): + with open('freqtrade/tests/testdata/BTC_ETH-1.json') as data_file: + return parse_ticker_dataframe(json.load(data_file)) + + +def test_default_strategy_class_name(): + assert class_name == DefaultStrategy.__name__ + + +def test_default_strategy_structure(): + assert hasattr(DefaultStrategy, 'minimal_roi') + assert hasattr(DefaultStrategy, 'stoploss') + assert hasattr(DefaultStrategy, 'ticker_interval') + assert hasattr(DefaultStrategy, 'populate_indicators') + assert hasattr(DefaultStrategy, 'populate_buy_trend') + assert hasattr(DefaultStrategy, 'populate_sell_trend') + + +def test_default_strategy(result): + strategy = DefaultStrategy() + + assert type(strategy.minimal_roi) is dict + assert type(strategy.stoploss) is float + assert type(strategy.ticker_interval) is int + indicators = strategy.populate_indicators(result) + assert type(indicators) is DataFrame + assert type(strategy.populate_buy_trend(indicators)) is DataFrame + assert type(strategy.populate_sell_trend(indicators)) is DataFrame diff --git a/freqtrade/tests/strategy/test_strategy.py b/freqtrade/tests/strategy/test_strategy.py new file mode 100644 index 000000000..79f045a6d --- /dev/null +++ b/freqtrade/tests/strategy/test_strategy.py @@ -0,0 +1,141 @@ +import json +import logging +import pytest +from freqtrade.strategy.strategy import Strategy +from freqtrade.analyze import parse_ticker_dataframe + + +@pytest.fixture +def result(): + with open('freqtrade/tests/testdata/BTC_ETH-1.json') as data_file: + return parse_ticker_dataframe(json.load(data_file)) + + +def test_sanitize_module_name(): + assert Strategy._sanitize_module_name('default_strategy') == 'default_strategy' + assert Strategy._sanitize_module_name('default_strategy.py') == 'default_strategy' + assert Strategy._sanitize_module_name('../default_strategy.py') == 'default_strategy' + assert Strategy._sanitize_module_name('../default_strategy') == 'default_strategy' + assert Strategy._sanitize_module_name('.default_strategy') == '.default_strategy' + assert Strategy._sanitize_module_name('foo-bar') == 'foo-bar' + assert Strategy._sanitize_module_name('foo/bar') == 'bar' + + +def test_search_strategy(): + assert Strategy._search_strategy('default_strategy') == '.' + assert Strategy._search_strategy('super_duper') is None + + +def test_strategy_structure(): + assert hasattr(Strategy, 'init') + assert hasattr(Strategy, 'minimal_roi') + assert hasattr(Strategy, 'stoploss') + assert hasattr(Strategy, 'populate_indicators') + assert hasattr(Strategy, 'populate_buy_trend') + assert hasattr(Strategy, 'populate_sell_trend') + + +def test_load_strategy(result): + strategy = Strategy() + strategy.logger = logging.getLogger(__name__) + + assert not hasattr(Strategy, 'custom_strategy') + strategy._load_strategy('default_strategy') + + assert not hasattr(Strategy, 'custom_strategy') + + assert hasattr(strategy.custom_strategy, 'populate_indicators') + assert 'adx' in strategy.populate_indicators(result) + + +def test_strategy(result): + strategy = Strategy() + strategy.init({'strategy': 'default_strategy'}) + + assert hasattr(strategy.custom_strategy, 'minimal_roi') + assert strategy.minimal_roi['0'] == 0.04 + + assert hasattr(strategy.custom_strategy, 'stoploss') + assert strategy.stoploss == -0.10 + + assert hasattr(strategy.custom_strategy, 'populate_indicators') + assert 'adx' in strategy.populate_indicators(result) + + assert hasattr(strategy.custom_strategy, 'populate_buy_trend') + dataframe = strategy.populate_buy_trend(strategy.populate_indicators(result)) + assert 'buy' in dataframe.columns + + assert hasattr(strategy.custom_strategy, 'populate_sell_trend') + dataframe = strategy.populate_sell_trend(strategy.populate_indicators(result)) + assert 'sell' in dataframe.columns + + +def test_strategy_override_minimal_roi(caplog): + config = { + 'strategy': 'default_strategy', + 'minimal_roi': { + "0": 0.5 + } + } + strategy = Strategy() + strategy.init(config) + + assert hasattr(strategy.custom_strategy, 'minimal_roi') + assert strategy.minimal_roi['0'] == 0.5 + assert ('freqtrade.strategy.strategy', + logging.INFO, + 'Override strategy \'minimal_roi\' with value in config file.' + ) in caplog.record_tuples + + +def test_strategy_override_stoploss(caplog): + config = { + 'strategy': 'default_strategy', + 'stoploss': -0.5 + } + strategy = Strategy() + strategy.init(config) + + assert hasattr(strategy.custom_strategy, 'stoploss') + assert strategy.stoploss == -0.5 + assert ('freqtrade.strategy.strategy', + logging.INFO, + 'Override strategy \'stoploss\' with value in config file: -0.5.' + ) in caplog.record_tuples + + +def test_strategy_override_ticker_interval(caplog): + config = { + 'strategy': 'default_strategy', + 'ticker_interval': 60 + } + strategy = Strategy() + strategy.init(config) + + assert hasattr(strategy.custom_strategy, 'ticker_interval') + assert strategy.ticker_interval == 60 + assert ('freqtrade.strategy.strategy', + logging.INFO, + 'Override strategy \'ticker_interval\' with value in config file: 60.' + ) in caplog.record_tuples + + +def test_strategy_fallback_default_strategy(): + strategy = Strategy() + strategy.logger = logging.getLogger(__name__) + + assert not hasattr(Strategy, 'custom_strategy') + strategy._load_strategy('../../super_duper') + assert not hasattr(Strategy, 'custom_strategy') + + +def test_strategy_singleton(): + strategy1 = Strategy() + strategy1.init({'strategy': 'default_strategy'}) + + assert hasattr(strategy1.custom_strategy, 'minimal_roi') + assert strategy1.minimal_roi['0'] == 0.04 + + strategy2 = Strategy() + assert hasattr(strategy2.custom_strategy, 'minimal_roi') + assert strategy2.minimal_roi['0'] == 0.04 diff --git a/freqtrade/tests/test_analyze.py b/freqtrade/tests/test_analyze.py index 8da38fcd7..472b5eff5 100644 --- a/freqtrade/tests/test_analyze.py +++ b/freqtrade/tests/test_analyze.py @@ -9,6 +9,7 @@ from pandas import DataFrame from freqtrade.analyze import (get_signal, parse_ticker_dataframe, populate_buy_trend, populate_indicators, populate_sell_trend) +from freqtrade.strategy.strategy import Strategy @pytest.fixture @@ -27,11 +28,17 @@ def test_dataframe_correct_length(result): def test_populates_buy_trend(result): + # Load the default strategy for the unit test, because this logic is done in main.py + Strategy().init({'strategy': 'default_strategy'}) + dataframe = populate_buy_trend(populate_indicators(result)) assert 'buy' in dataframe.columns def test_populates_sell_trend(result): + # Load the default strategy for the unit test, because this logic is done in main.py + Strategy().init({'strategy': 'default_strategy'}) + dataframe = populate_sell_trend(populate_indicators(result)) assert 'sell' in dataframe.columns @@ -72,3 +79,16 @@ def test_get_signal_handles_exceptions(mocker): side_effect=Exception('invalid ticker history ')) assert get_signal('BTC-ETH', 5) == (False, False) + + +def test_parse_ticker_dataframe(ticker_history, ticker_history_without_bv): + + columns = ['close', 'high', 'low', 'open', 'date', 'volume'] + + # Test file with BV data + dataframe = parse_ticker_dataframe(ticker_history) + assert dataframe.columns.tolist() == columns + + # Test file without BV data + dataframe = parse_ticker_dataframe(ticker_history_without_bv) + assert dataframe.columns.tolist() == columns diff --git a/freqtrade/tests/test_fiat_convert.py b/freqtrade/tests/test_fiat_convert.py index ddc1c8e29..2d112f921 100644 --- a/freqtrade/tests/test_fiat_convert.py +++ b/freqtrade/tests/test_fiat_convert.py @@ -116,9 +116,9 @@ def test_fiat_convert_get_price(mocker): assert fiat_convert._pairs[0]._expiration is not expiration -def test_fiat_convert_without_network(mocker): - pymarketcap = MagicMock(side_effect=ImportError('Oh boy, you have no network!')) - mocker.patch('freqtrade.fiat_convert.Pymarketcap', pymarketcap) +def test_fiat_convert_without_network(): + # Because CryptoToFiatConverter is a Singleton we reset the value of _coinmarketcap + CryptoToFiatConverter._coinmarketcap = None fiat_convert = CryptoToFiatConverter() assert fiat_convert._coinmarketcap is None diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index a61342480..23f4cd259 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -525,9 +525,7 @@ def test_execute_sell_up(default_conf, ticker, ticker_sell_up, mocker): mocker.patch.multiple('freqtrade.main.exchange', validate_pairs=MagicMock(), get_ticker=ticker) - mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap', - ticker=MagicMock(return_value={'price_usd': 15000.0}), - _cache_symbols=MagicMock(return_value={'BTC': 1})) + mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) init(default_conf, create_engine('sqlite://')) # Create some test data @@ -544,7 +542,10 @@ def test_execute_sell_up(default_conf, ticker, ticker_sell_up, mocker): execute_sell(trade=trade, limit=ticker_sell_up()['bid']) assert rpc_mock.call_count == 2 - assert 'Selling [BTC/ETH]' in rpc_mock.call_args_list[-1][0][0] + assert 'Selling' in rpc_mock.call_args_list[-1][0][0] + assert '[BTC_ETH]' in rpc_mock.call_args_list[-1][0][0] + assert 'Amount' in rpc_mock.call_args_list[-1][0][0] + assert 'Profit' in rpc_mock.call_args_list[-1][0][0] assert '0.00001172' in rpc_mock.call_args_list[-1][0][0] assert 'profit: 6.11%, 0.00006126' in rpc_mock.call_args_list[-1][0][0] assert '0.919 USD' in rpc_mock.call_args_list[-1][0][0] @@ -562,9 +563,7 @@ def test_execute_sell_down(default_conf, ticker, ticker_sell_down, mocker): mocker.patch.multiple('freqtrade.main.exchange', validate_pairs=MagicMock(), get_ticker=ticker) - mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap', - ticker=MagicMock(return_value={'price_usd': 15000.0}), - _cache_symbols=MagicMock(return_value={'BTC': 1})) + mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) init(default_conf, create_engine('sqlite://')) # Create some test data @@ -581,7 +580,9 @@ def test_execute_sell_down(default_conf, ticker, ticker_sell_down, mocker): execute_sell(trade=trade, limit=ticker_sell_down()['bid']) assert rpc_mock.call_count == 2 - assert 'Selling [BTC/ETH]' in rpc_mock.call_args_list[-1][0][0] + assert 'Selling' in rpc_mock.call_args_list[-1][0][0] + assert '[BTC_ETH]' in rpc_mock.call_args_list[-1][0][0] + assert 'Amount' in rpc_mock.call_args_list[-1][0][0] assert '0.00001044' in rpc_mock.call_args_list[-1][0][0] assert 'loss: -5.48%, -0.00005492' in rpc_mock.call_args_list[-1][0][0] assert '-0.824 USD' in rpc_mock.call_args_list[-1][0][0] @@ -611,10 +612,9 @@ def test_execute_sell_without_conf_sell_down(default_conf, ticker, ticker_sell_d execute_sell(trade=trade, limit=ticker_sell_down()['bid']) - print(rpc_mock.call_args_list[-1][0][0]) - assert rpc_mock.call_count == 2 - assert 'Selling [BTC/ETH]' in rpc_mock.call_args_list[-1][0][0] + assert 'Selling' in rpc_mock.call_args_list[-1][0][0] + assert '[BTC_ETH]' in rpc_mock.call_args_list[-1][0][0] assert '0.00001044' in rpc_mock.call_args_list[-1][0][0] assert 'loss: -5.48%, -0.00005492' in rpc_mock.call_args_list[-1][0][0] @@ -644,7 +644,9 @@ def test_execute_sell_without_conf_sell_up(default_conf, ticker, ticker_sell_up, execute_sell(trade=trade, limit=ticker_sell_up()['bid']) assert rpc_mock.call_count == 2 - assert 'Selling [BTC/ETH]' in rpc_mock.call_args_list[-1][0][0] + assert 'Selling' in rpc_mock.call_args_list[-1][0][0] + assert '[BTC_ETH]' in rpc_mock.call_args_list[-1][0][0] + assert 'Amount' in rpc_mock.call_args_list[-1][0][0] assert '0.00001172' in rpc_mock.call_args_list[-1][0][0] assert '(profit: 6.11%, 0.00006126)' in rpc_mock.call_args_list[-1][0][0] assert 'USD' not in rpc_mock.call_args_list[-1][0][0] diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py index 70797f960..bfc16064c 100644 --- a/freqtrade/tests/test_persistence.py +++ b/freqtrade/tests/test_persistence.py @@ -4,7 +4,7 @@ import os import pytest from freqtrade.exchange import Exchanges -from freqtrade.persistence import Trade, init +from freqtrade.persistence import Trade, init, clean_dry_run_db def test_init_create_session(default_conf, mocker): @@ -310,3 +310,50 @@ def test_calc_profit_percent(limit_buy_order, limit_sell_order): # Test with a custom fee rate on the close trade assert trade.calc_profit_percent(fee=0.003) == 0.0614782 + + +def test_clean_dry_run_db(default_conf, mocker): + init(default_conf) + + # Simulate dry_run entries + trade = Trade( + pair='BTC_ETH', + stake_amount=0.001, + amount=123.0, + fee=0.0025, + open_rate=0.123, + exchange='BITTREX', + open_order_id='dry_run_buy_12345' + ) + Trade.session.add(trade) + + trade = Trade( + pair='BTC_ETC', + stake_amount=0.001, + amount=123.0, + fee=0.0025, + open_rate=0.123, + exchange='BITTREX', + open_order_id='dry_run_sell_12345' + ) + Trade.session.add(trade) + + # Simulate prod entry + trade = Trade( + pair='BTC_ETC', + stake_amount=0.001, + amount=123.0, + fee=0.0025, + open_rate=0.123, + exchange='BITTREX', + open_order_id='prod_buy_12345' + ) + Trade.session.add(trade) + + # We have 3 entries: 2 dry_run, 1 prod + assert len(Trade.query.filter(Trade.open_order_id.isnot(None)).all()) == 3 + + clean_dry_run_db() + + # We have now only the prod + assert len(Trade.query.filter(Trade.open_order_id.isnot(None)).all()) == 1 diff --git a/requirements.txt b/requirements.txt index d37312268..b574c76ac 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,7 +19,7 @@ hyperopt==0.1 # do not upgrade networkx before this is fixed https://github.com/hyperopt/hyperopt/issues/325 networkx==1.11 tabulate==0.8.2 -pymarketcap==3.3.148 +pymarketcap==3.3.150 # Required for plotting data #matplotlib==2.1.0 diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index e90cd5dfe..64b508d55 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -3,14 +3,23 @@ import sys import logging import argparse + import matplotlib +# matplotlib.use("Qt5Agg") import matplotlib.dates as mdates import matplotlib.pyplot as plt +from pandas import DataFrame +import talib.abstract as ta + +import freqtrade.vendor.qtpylib.indicators as qtpylib from freqtrade import exchange, analyze +from freqtrade.misc import common_args_parser +from freqtrade.strategy.strategy import Strategy import freqtrade.misc as misc import freqtrade.optimize as optimize import freqtrade.analyze as analyze + logger = logging.getLogger(__name__) @@ -21,7 +30,7 @@ def plot_parse_args(args): return parser.parse_args(args) -def plot_analyzed_dataframe(args): +def plot_analyzed_dataframe(args) -> None: """ Calls analyze() and plots the returned dataframe :param pair: pair as str @@ -31,35 +40,40 @@ def plot_analyzed_dataframe(args): pairs = [pair] timerange = misc.parse_timerange(args.timerange) + # Init strategy + strategy = Strategy() + strategy.init({'strategy': args.strategy}) + tick_interval = strategy.ticker_interval + tickers = {} if args.live: logger.info('Downloading pair.') + # Init Bittrex to use public API exchange._API = exchange.Bittrex({'key': '', 'secret': ''}) - tickers[pair] = exchange.get_ticker_history(pair, args.ticker_interval) + tickers[pair] = exchange.get_ticker_history(pair, tick_interval) else: tickers = optimize.load_data(args.datadir, pairs=pairs, - ticker_interval=args.ticker_interval, + ticker_interval=tick_interval, refresh_pairs=False, timerange=timerange) dataframes = optimize.tickerdata_to_dataframe(tickers) dataframe = dataframes[pair] dataframe = analyze.populate_buy_trend(dataframe) dataframe = analyze.populate_sell_trend(dataframe) - dates = misc.datesarray_to_datetimearray(dataframe['date']) - dataframe.loc[dataframe['buy'] == 1, 'buy_price'] = dataframe['close'] - dataframe.loc[dataframe['sell'] == 1, 'sell_price'] = dataframe['close'] - # Two subplots sharing x axis fig, (ax1, ax2, ax3) = plt.subplots(3, sharex=True) - fig.suptitle(pair + " " + str(args.ticker_interval), fontsize=14, fontweight='bold') + fig.suptitle(pair + " " + str(tick_interval), fontsize=14, fontweight='bold') + ax1.plot(dates, dataframe['close'], label='close') # ax1.plot(dates, dataframe['sell'], 'ro', label='sell') ax1.plot(dates, dataframe['sma'], '--', label='SMA') ax1.plot(dates, dataframe['tema'], ':', label='TEMA') ax1.plot(dates, dataframe['blower'], '-.', label='BB low') - ax1.plot(dates, dataframe['buy_price'], 'bo', label='buy') + ax1.plot(dates, dataframe['close'] * dataframe['buy'], 'bo', label='buy') + ax1.plot(dates, dataframe['close'] * dataframe['sell'], 'ro', label='sell') + ax1.legend() ax2.plot(dates, dataframe['adx'], label='ADX') @@ -81,7 +95,6 @@ def plot_analyzed_dataframe(args): plt.setp([a.get_xticklabels() for a in fig.axes[:-1]], visible=False) plt.show() - if __name__ == '__main__': args = plot_parse_args(sys.argv[1:]) plot_analyzed_dataframe(args) diff --git a/scripts/plot_profit.py b/scripts/plot_profit.py index 08941cb2a..29dda8961 100755 --- a/scripts/plot_profit.py +++ b/scripts/plot_profit.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 import sys -import argparse import json import matplotlib.pyplot as plt import matplotlib.dates as mdates @@ -10,6 +9,7 @@ import numpy as np import freqtrade.optimize as optimize import freqtrade.misc as misc import freqtrade.exchange as exchange +from freqtrade.strategy.strategy import Strategy def plot_parse_args(args): @@ -44,7 +44,7 @@ def make_profit_array(data, px, filter_pairs=[]): # total profits at each timeframe # to accumulated profits pa = 0 - for x in range(0,len(pg)): + for x in range(0, len(pg)): p = pg[x] # Get current total percent pa += p # Add to the accumulated percent pg[x] = pa # write back to save memory @@ -67,7 +67,14 @@ def plot_profit(args) -> None: filter_pairs = args.pair config = misc.load_config(args.config) + config.update({'strategy': args.strategy}) + + # Init strategy + strategy = Strategy() + strategy.init(config) + pairs = config['exchange']['pair_whitelist'] + if filter_pairs: filter_pairs = filter_pairs.split(',') pairs = list(set(pairs) & set(filter_pairs)) @@ -75,7 +82,7 @@ def plot_profit(args) -> None: timerange = misc.parse_timerange(args.timerange) tickers = optimize.load_data(args.datadir, pairs=pairs, - ticker_interval=args.ticker_interval, + ticker_interval=strategy.ticker_interval, refresh_pairs=False, timerange=timerange) dataframes = optimize.preprocess(tickers) @@ -96,7 +103,7 @@ def plot_profit(args) -> None: for pair, pair_data in dataframes.items(): close = pair_data['close'] maxprice = max(close) # Normalize price to [0,1] - print('Pair %s has length %s' %(pair, len(close))) + print('Pair %s has length %s' % (pair, len(close))) for x in range(0, len(close)): avgclose[x] += close[x] / maxprice # avgclose += close @@ -108,7 +115,7 @@ def plot_profit(args) -> None: filename = 'backtest-result.json' with open(filename) as file: - data = json.load(file) + data = json.load(file) pg = make_profit_array(data, max_x, filter_pairs) # diff --git a/user_data/data/.gitkeep b/user_data/data/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/freqtrade/optimize/hyperopt_conf.py b/user_data/hyperopt_conf.py similarity index 100% rename from freqtrade/optimize/hyperopt_conf.py rename to user_data/hyperopt_conf.py diff --git a/user_data/strategies/__init__.py b/user_data/strategies/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/user_data/strategies/test_strategy.py b/user_data/strategies/test_strategy.py new file mode 100644 index 000000000..a164812c4 --- /dev/null +++ b/user_data/strategies/test_strategy.py @@ -0,0 +1,246 @@ + +# --- Do not remove these libs --- +from freqtrade.strategy.interface import IStrategy +from pandas import DataFrame +# -------------------------------- + +# Add your lib to import here +import talib.abstract as ta +import freqtrade.vendor.qtpylib.indicators as qtpylib +import numpy # noqa + + +# Update this variable if you change the class name +class_name = 'TestStrategy' + + +# This class is a sample. Feel free to customize it. +class TestStrategy(IStrategy): + """ + This is a test strategy to inspire you. + More information in https://github.com/gcarq/freqtrade/blob/develop/docs/bot-optimization.md + + You can: + - Rename the class name (Do not forget to update class_name) + - Add any methods you want to build your strategy + - Add any lib you need to build your strategy + + You must keep: + - the lib in the section "Do not remove these libs" + - the prototype for the methods: minimal_roi, stoploss, populate_indicators, populate_buy_trend, + populate_sell_trend, hyperopt_space, buy_strategy_generator + """ + + # Minimal ROI designed for the strategy. + # This attribute will be overridden if the config file contains "minimal_roi" + minimal_roi = { + "40": 0.0, + "30": 0.01, + "20": 0.02, + "0": 0.04 + } + + # Optimal stoploss designed for the strategy + # This attribute will be overridden if the config file contains "stoploss" + stoploss = -0.10 + + # Optimal ticker interval for the strategy + ticker_interval = 5 + + def populate_indicators(self, dataframe: DataFrame) -> 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. + """ + + # Momentum Indicator + # ------------------------------------ + + # ADX + dataframe['adx'] = ta.ADX(dataframe) + + """ + # Awesome oscillator + dataframe['ao'] = qtpylib.awesome_oscillator(dataframe) + + # Commodity Channel Index: values Oversold:<-100, Overbought:>100 + dataframe['cci'] = ta.CCI(dataframe) + + # MACD + macd = ta.MACD(dataframe) + dataframe['macd'] = macd['macd'] + dataframe['macdsignal'] = macd['macdsignal'] + dataframe['macdhist'] = macd['macdhist'] + + # MFI + dataframe['mfi'] = ta.MFI(dataframe) + + # Minus Directional Indicator / Movement + dataframe['minus_dm'] = ta.MINUS_DM(dataframe) + dataframe['minus_di'] = ta.MINUS_DI(dataframe) + + # Plus Directional Indicator / Movement + dataframe['plus_dm'] = ta.PLUS_DM(dataframe) + dataframe['plus_di'] = ta.PLUS_DI(dataframe) + dataframe['minus_di'] = ta.MINUS_DI(dataframe) + + # ROC + dataframe['roc'] = ta.ROC(dataframe) + + # 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) + + # Inverse Fisher transform on RSI normalized, value [0.0, 100.0] (https://goo.gl/2JGGoy) + dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1) + + # Stoch + stoch = ta.STOCH(dataframe) + dataframe['slowd'] = stoch['slowd'] + dataframe['slowk'] = stoch['slowk'] + + # Stoch fast + stoch_fast = ta.STOCHF(dataframe) + dataframe['fastd'] = stoch_fast['fastd'] + dataframe['fastk'] = stoch_fast['fastk'] + + # Stoch RSI + stoch_rsi = ta.STOCHRSI(dataframe) + dataframe['fastd_rsi'] = stoch_rsi['fastd'] + dataframe['fastk_rsi'] = stoch_rsi['fastk'] + """ + + # Overlap Studies + # ------------------------------------ + + # Bollinger bands + bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) + dataframe['bb_lowerband'] = bollinger['lower'] + dataframe['bb_middleband'] = bollinger['mid'] + dataframe['bb_upperband'] = bollinger['upper'] + + """ + # EMA - Exponential Moving Average + dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3) + dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5) + dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10) + dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50) + dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100) + + # SAR Parabol + dataframe['sar'] = ta.SAR(dataframe) + + # SMA - Simple Moving Average + dataframe['sma'] = ta.SMA(dataframe, timeperiod=40) + """ + + # TEMA - Triple Exponential Moving Average + dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9) + + # Cycle Indicator + # ------------------------------------ + # Hilbert Transform Indicator - SineWave + hilbert = ta.HT_SINE(dataframe) + dataframe['htsine'] = hilbert['sine'] + dataframe['htleadsine'] = hilbert['leadsine'] + + # Pattern Recognition - Bullish candlestick patterns + # ------------------------------------ + """ + # Hammer: values [0, 100] + dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe) + # Inverted Hammer: values [0, 100] + dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe) + # Dragonfly Doji: values [0, 100] + dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe) + # Piercing Line: values [0, 100] + dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100] + # Morningstar: values [0, 100] + dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100] + # Three White Soldiers: values [0, 100] + dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100] + """ + + # Pattern Recognition - Bearish candlestick patterns + # ------------------------------------ + """ + # Hanging Man: values [0, 100] + dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe) + # Shooting Star: values [0, 100] + dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe) + # Gravestone Doji: values [0, 100] + dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe) + # Dark Cloud Cover: values [0, 100] + dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe) + # Evening Doji Star: values [0, 100] + dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe) + # Evening Star: values [0, 100] + dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe) + """ + + # Pattern Recognition - Bullish/Bearish candlestick patterns + # ------------------------------------ + """ + # Three Line Strike: values [0, -100, 100] + dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe) + # Spinning Top: values [0, -100, 100] + dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100] + # Engulfing: values [0, -100, 100] + dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100] + # Harami: values [0, -100, 100] + dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100] + # Three Outside Up/Down: values [0, -100, 100] + dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100] + # Three Inside Up/Down: values [0, -100, 100] + dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100] + """ + + # Chart type + # ------------------------------------ + """ + # Heikinashi stategy + heikinashi = qtpylib.heikinashi(dataframe) + dataframe['ha_open'] = heikinashi['open'] + dataframe['ha_close'] = heikinashi['close'] + dataframe['ha_high'] = heikinashi['high'] + dataframe['ha_low'] = heikinashi['low'] + """ + + return dataframe + + def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: + """ + Based on TA indicators, populates the buy signal for the given dataframe + :param dataframe: DataFrame + :return: DataFrame with buy column + """ + dataframe.loc[ + ( + (dataframe['adx'] > 30) & + (dataframe['tema'] <= dataframe['bb_middleband']) & + (dataframe['tema'] > dataframe['tema'].shift(1)) + ), + 'buy'] = 1 + + return dataframe + + def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame: + """ + Based on TA indicators, populates the sell signal for the given dataframe + :param dataframe: DataFrame + :return: DataFrame with buy column + """ + dataframe.loc[ + ( + (dataframe['adx'] > 70) & + (dataframe['tema'] > dataframe['bb_middleband']) & + (dataframe['tema'] < dataframe['tema'].shift(1)) + ), + 'sell'] = 1 + return dataframe