commit
5e4a6ba7ba
@ -5,6 +5,7 @@
|
|||||||
"fiat_display_currency": "USD",
|
"fiat_display_currency": "USD",
|
||||||
"ticker_interval" : "5m",
|
"ticker_interval" : "5m",
|
||||||
"dry_run": false,
|
"dry_run": false,
|
||||||
|
"trailing_stop": false,
|
||||||
"unfilledtimeout": {
|
"unfilledtimeout": {
|
||||||
"buy": 10,
|
"buy": 10,
|
||||||
"sell": 30
|
"sell": 30
|
||||||
|
@ -5,6 +5,8 @@
|
|||||||
"fiat_display_currency": "USD",
|
"fiat_display_currency": "USD",
|
||||||
"dry_run": false,
|
"dry_run": false,
|
||||||
"ticker_interval": "5m",
|
"ticker_interval": "5m",
|
||||||
|
"trailing_stop": false,
|
||||||
|
"trailing_stop_positive": 0.005,
|
||||||
"minimal_roi": {
|
"minimal_roi": {
|
||||||
"40": 0.0,
|
"40": 0.0,
|
||||||
"30": 0.01,
|
"30": 0.01,
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
# Configure the bot
|
# Configure the bot
|
||||||
|
|
||||||
This page explains how to configure your `config.json` file.
|
This page explains how to configure your `config.json` file.
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
- [Bot commands](#bot-commands)
|
- [Bot commands](#bot-commands)
|
||||||
- [Backtesting commands](#backtesting-commands)
|
- [Backtesting commands](#backtesting-commands)
|
||||||
- [Hyperopt commands](#hyperopt-commands)
|
- [Hyperopt commands](#hyperopt-commands)
|
||||||
|
|
||||||
## Setup config.json
|
## Setup config.json
|
||||||
|
|
||||||
We recommend to copy and use the `config.json.example` as a template
|
We recommend to copy and use the `config.json.example` as a template
|
||||||
for your bot configuration.
|
for your bot configuration.
|
||||||
|
|
||||||
@ -22,6 +25,8 @@ The table below will list all configuration parameters.
|
|||||||
| `dry_run` | true | Yes | Define if the bot must be in Dry-run or production mode.
|
| `dry_run` | true | Yes | Define if the bot must be in Dry-run or production mode.
|
||||||
| `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.
|
| `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.
|
| `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.
|
||||||
|
| `trailing_stoploss` | false | No | Enables trailing stop-loss (based on `stoploss` in either configuration or strategy file).
|
||||||
|
| `trailing_stoploss_positve` | 0 | No | Changes stop-loss once profit has been reached.
|
||||||
| `unfilledtimeout.buy` | 10 | Yes | How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled.
|
| `unfilledtimeout.buy` | 10 | Yes | How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled.
|
||||||
| `unfilledtimeout.sell` | 10 | Yes | How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled.
|
| `unfilledtimeout.sell` | 10 | Yes | How long (in minutes) the bot will wait for an unfilled sell 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.
|
| `bid_strategy.ask_last_balance` | 0.0 | Yes | Set the bidding price. More information below.
|
||||||
@ -42,10 +47,10 @@ The table below will list all configuration parameters.
|
|||||||
| `strategy_path` | null | No | Adds an additional strategy lookup path (must be a folder).
|
| `strategy_path` | null | No | Adds an additional strategy lookup path (must be a folder).
|
||||||
| `internals.process_throttle_secs` | 5 | Yes | Set the process throttle. Value in second.
|
| `internals.process_throttle_secs` | 5 | Yes | Set the process throttle. Value in second.
|
||||||
|
|
||||||
The definition of each config parameters is in
|
The definition of each config parameters is in [misc.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/misc.py#L205).
|
||||||
[misc.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/misc.py#L205).
|
|
||||||
|
|
||||||
### Understand stake_amount
|
### Understand stake_amount
|
||||||
|
|
||||||
`stake_amount` is an amount of crypto-currency your bot will use for each trade.
|
`stake_amount` is an amount of crypto-currency your bot will use for each trade.
|
||||||
The minimal value is 0.0005. If there is not enough crypto-currency in
|
The minimal value is 0.0005. If there is not enough crypto-currency in
|
||||||
the account an exception is generated.
|
the account an exception is generated.
|
||||||
@ -53,9 +58,11 @@ To allow the bot to trade all the avaliable `stake_currency` in your account set
|
|||||||
In this case a trade amount is calclulated as `currency_balanse / (max_open_trades - current_open_trades)`.
|
In this case a trade amount is calclulated as `currency_balanse / (max_open_trades - current_open_trades)`.
|
||||||
|
|
||||||
### Understand minimal_roi
|
### Understand minimal_roi
|
||||||
|
|
||||||
`minimal_roi` is a JSON object where the key is a duration
|
`minimal_roi` is a JSON object where the key is a duration
|
||||||
in minutes and the value is the minimum ROI in percent.
|
in minutes and the value is the minimum ROI in percent.
|
||||||
See the example below:
|
See the example below:
|
||||||
|
|
||||||
```
|
```
|
||||||
"minimal_roi": {
|
"minimal_roi": {
|
||||||
"40": 0.0, # Sell after 40 minutes if the profit is not negative
|
"40": 0.0, # Sell after 40 minutes if the profit is not negative
|
||||||
@ -70,6 +77,7 @@ value. This parameter is optional. If you use it, it will take over the
|
|||||||
`minimal_roi` value from the strategy file.
|
`minimal_roi` value from the strategy file.
|
||||||
|
|
||||||
### Understand stoploss
|
### Understand stoploss
|
||||||
|
|
||||||
`stoploss` is loss in percentage that should trigger a sale.
|
`stoploss` is loss in percentage that should trigger a sale.
|
||||||
For example value `-0.10` will cause immediate sell if the
|
For example value `-0.10` will cause immediate sell if the
|
||||||
profit dips below -10% for a given trade. This parameter is optional.
|
profit dips below -10% for a given trade. This parameter is optional.
|
||||||
@ -78,56 +86,70 @@ Most of the strategy files already include the optimal `stoploss`
|
|||||||
value. This parameter is optional. If you use it, it will take over the
|
value. This parameter is optional. If you use it, it will take over the
|
||||||
`stoploss` value from the strategy file.
|
`stoploss` value from the strategy file.
|
||||||
|
|
||||||
|
### Understand trailing stoploss
|
||||||
|
|
||||||
|
Go to the [trailing stoploss Documentation](stoploss.md) for details on trailing stoploss.
|
||||||
|
|
||||||
### Understand initial_state
|
### Understand initial_state
|
||||||
|
|
||||||
`initial_state` is an optional field that defines the initial application state.
|
`initial_state` is an optional field that defines the initial application state.
|
||||||
Possible values are `running` or `stopped`. (default=`running`)
|
Possible values are `running` or `stopped`. (default=`running`)
|
||||||
If the value is `stopped` the bot has to be started with `/start` first.
|
If the value is `stopped` the bot has to be started with `/start` first.
|
||||||
|
|
||||||
### Understand process_throttle_secs
|
### Understand process_throttle_secs
|
||||||
|
|
||||||
`process_throttle_secs` is an optional field that defines in seconds how long the bot should wait
|
`process_throttle_secs` is an optional field that defines in seconds how long the bot should wait
|
||||||
before asking the strategy if we should buy or a sell an asset. After each wait period, the strategy is asked again for
|
before asking the strategy if we should buy or a sell an asset. After each wait period, the strategy is asked again for
|
||||||
every opened trade wether or not we should sell, and for all the remaining pairs (either the dynamic list of pairs or
|
every opened trade wether or not we should sell, and for all the remaining pairs (either the dynamic list of pairs or
|
||||||
the static list of pairs) if we should buy.
|
the static list of pairs) if we should buy.
|
||||||
|
|
||||||
### Understand ask_last_balance
|
### Understand ask_last_balance
|
||||||
|
|
||||||
`ask_last_balance` sets the bidding price. Value `0.0` will use `ask` price, `1.0` will
|
`ask_last_balance` sets the bidding price. Value `0.0` will use `ask` price, `1.0` will
|
||||||
use the `last` price and values between those interpolate between ask and last
|
use the `last` price and values between those interpolate between ask and last
|
||||||
price. Using `ask` price will guarantee quick success in bid, but bot will also
|
price. Using `ask` price will guarantee quick success in bid, but bot will also
|
||||||
end up paying more then would probably have been necessary.
|
end up paying more then would probably have been necessary.
|
||||||
|
|
||||||
### What values for exchange.name?
|
### What values for exchange.name?
|
||||||
|
|
||||||
Freqtrade is based on [CCXT library](https://github.com/ccxt/ccxt) that supports 115 cryptocurrency
|
Freqtrade is based on [CCXT library](https://github.com/ccxt/ccxt) that supports 115 cryptocurrency
|
||||||
exchange markets and trading APIs. The complete up-to-date list can be found in the
|
exchange markets and trading APIs. The complete up-to-date list can be found in the
|
||||||
[CCXT repo homepage](https://github.com/ccxt/ccxt/tree/master/python). However, the bot was tested
|
[CCXT repo homepage](https://github.com/ccxt/ccxt/tree/master/python). However, the bot was tested
|
||||||
with only Bittrex and Binance.
|
with only Bittrex and Binance.
|
||||||
|
|
||||||
The bot was tested with the following exchanges:
|
The bot was tested with the following exchanges:
|
||||||
|
|
||||||
- [Bittrex](https://bittrex.com/): "bittrex"
|
- [Bittrex](https://bittrex.com/): "bittrex"
|
||||||
- [Binance](https://www.binance.com/): "binance"
|
- [Binance](https://www.binance.com/): "binance"
|
||||||
|
|
||||||
Feel free to test other exchanges and submit your PR to improve the bot.
|
Feel free to test other exchanges and submit your PR to improve the bot.
|
||||||
|
|
||||||
### What values for fiat_display_currency?
|
### What values for fiat_display_currency?
|
||||||
|
|
||||||
`fiat_display_currency` set the base currency to use for the conversion from coin to fiat in Telegram.
|
`fiat_display_currency` set the base currency to use for the conversion from coin to fiat in Telegram.
|
||||||
The valid values are: "AUD", "BRL", "CAD", "CHF", "CLP", "CNY", "CZK", "DKK", "EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY", "KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN", "RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD".
|
The valid values are: "AUD", "BRL", "CAD", "CHF", "CLP", "CNY", "CZK", "DKK", "EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY", "KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN", "RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD".
|
||||||
In addition to central bank currencies, a range of cryto currencies are supported.
|
In addition to central bank currencies, a range of cryto currencies are supported.
|
||||||
The valid values are: "BTC", "ETH", "XRP", "LTC", "BCH", "USDT".
|
The valid values are: "BTC", "ETH", "XRP", "LTC", "BCH", "USDT".
|
||||||
|
|
||||||
## Switch to dry-run mode
|
## Switch to dry-run mode
|
||||||
|
|
||||||
We recommend starting the bot in dry-run mode to see how your bot will
|
We recommend starting the bot in dry-run mode to see how your bot will
|
||||||
behave and how is the performance of your strategy. In Dry-run mode the
|
behave and how is the performance of your strategy. In Dry-run mode the
|
||||||
bot does not engage your money. It only runs a live simulation without
|
bot does not engage your money. It only runs a live simulation without
|
||||||
creating trades.
|
creating trades.
|
||||||
|
|
||||||
### To switch your bot in Dry-run mode:
|
### To switch your bot in Dry-run mode:
|
||||||
|
|
||||||
1. Edit your `config.json` file
|
1. Edit your `config.json` file
|
||||||
2. Switch dry-run to true and specify db_url for a persistent db
|
2. Switch dry-run to true and specify db_url for a persistent db
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"dry_run": true,
|
"dry_run": true,
|
||||||
"db_url": "sqlite///tradesv3.dryrun.sqlite",
|
"db_url": "sqlite///tradesv3.dryrun.sqlite",
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Remove your Exchange API key (change them by fake api credentials)
|
3. Remove your Exchange API key (change them by fake api credentials)
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"exchange": {
|
"exchange": {
|
||||||
"name": "bittrex",
|
"name": "bittrex",
|
||||||
@ -141,19 +163,23 @@ Once you will be happy with your bot performance, you can switch it to
|
|||||||
production mode.
|
production mode.
|
||||||
|
|
||||||
## Switch to production mode
|
## Switch to production mode
|
||||||
|
|
||||||
In production mode, the bot will engage your money. Be careful a wrong
|
In production mode, the bot will engage your money. Be careful a wrong
|
||||||
strategy can lose all your money. Be aware of what you are doing when
|
strategy can lose all your money. Be aware of what you are doing when
|
||||||
you run it in production mode.
|
you run it in production mode.
|
||||||
|
|
||||||
### To switch your bot in production mode:
|
### To switch your bot in production mode:
|
||||||
|
|
||||||
1. Edit your `config.json` file
|
1. Edit your `config.json` file
|
||||||
|
|
||||||
2. Switch dry-run to false and don't forget to adapt your database URL if set
|
2. Switch dry-run to false and don't forget to adapt your database URL if set
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"dry_run": false,
|
"dry_run": false,
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Insert your Exchange API key (change them by fake api keys)
|
3. Insert your Exchange API key (change them by fake api keys)
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"exchange": {
|
"exchange": {
|
||||||
"name": "bittrex",
|
"name": "bittrex",
|
||||||
@ -161,10 +187,10 @@ you run it in production mode.
|
|||||||
"secret": "08a9dc6db3d7b53e1acebd9275677f4b0a04f1a5",
|
"secret": "08a9dc6db3d7b53e1acebd9275677f4b0a04f1a5",
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
|
|
||||||
```
|
```
|
||||||
If you have not your Bittrex API key yet,
|
If you have not your Bittrex API key yet, [see our tutorial](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md).
|
||||||
[see our tutorial](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md).
|
|
||||||
|
|
||||||
## Next step
|
## Next step
|
||||||
Now you have configured your config.json, the next step is to
|
|
||||||
[start your bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md).
|
Now you have configured your config.json, the next step is to [start your bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md).
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
# freqtrade documentation
|
# freqtrade documentation
|
||||||
|
|
||||||
Welcome to freqtrade documentation. Please feel free to contribute to
|
Welcome to freqtrade documentation. Please feel free to contribute to
|
||||||
this documentation if you see it became outdated by sending us a
|
this documentation if you see it became outdated by sending us a
|
||||||
Pull-request. Do not hesitate to reach us on
|
Pull-request. Do not hesitate to reach us on
|
||||||
@ -6,6 +7,7 @@ Pull-request. Do not hesitate to reach us on
|
|||||||
if you do not find the answer to your questions.
|
if you do not find the answer to your questions.
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
- [Pre-requisite](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md)
|
- [Pre-requisite](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md)
|
||||||
- [Setup your Bittrex account](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md#setup-your-bittrex-account)
|
- [Setup your Bittrex account](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md#setup-your-bittrex-account)
|
||||||
- [Setup your Telegram bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md#setup-your-telegram-bot)
|
- [Setup your Telegram bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md#setup-your-telegram-bot)
|
||||||
|
48
docs/stoploss.md
Normal file
48
docs/stoploss.md
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
# Stop Loss support
|
||||||
|
|
||||||
|
At this stage the bot contains the following stoploss support modes:
|
||||||
|
|
||||||
|
1. static stop loss, defined in either the strategy or configuration
|
||||||
|
2. trailing stop loss, defined in the configuration
|
||||||
|
3. trailing stop loss, custom positive loss, defined in configuration
|
||||||
|
|
||||||
|
## Static Stop Loss
|
||||||
|
|
||||||
|
This is very simple, basically you define a stop loss of x in your strategy file or alternative in the configuration, which
|
||||||
|
will overwrite the strategy definition. This will basically try to sell your asset, the second the loss exceeds the defined loss.
|
||||||
|
|
||||||
|
## Trail Stop Loss
|
||||||
|
|
||||||
|
The initial value for this stop loss, is defined in your strategy or configuration. Just as you would define your Stop Loss normally.
|
||||||
|
To enable this Feauture all you have to do is to define the configuration element:
|
||||||
|
|
||||||
|
``` json
|
||||||
|
"trailing_stop" : True
|
||||||
|
```
|
||||||
|
|
||||||
|
This will now activate an algorithm, which automatically moves your stop loss up every time the price of your asset increases.
|
||||||
|
|
||||||
|
For example, simplified math,
|
||||||
|
|
||||||
|
* you buy an asset at a price of 100$
|
||||||
|
* your stop loss is defined at 2%
|
||||||
|
* which means your stop loss, gets triggered once your asset dropped below 98$
|
||||||
|
* assuming your asset now increases to 102$
|
||||||
|
* your stop loss, will now be 2% of 102$ or 99.96$
|
||||||
|
* now your asset drops in value to 101$, your stop loss, will still be 99.96$
|
||||||
|
|
||||||
|
basically what this means is that your stop loss will be adjusted to be always be 2% of the highest observed price
|
||||||
|
|
||||||
|
### Custom positive loss
|
||||||
|
|
||||||
|
Due to demand, it is possible to have a default stop loss, when you are in the red with your buy, but once your buy turns positive,
|
||||||
|
the system will utilize a new stop loss, which can be a different value. For example your default stop loss is 5%, but once you are in the
|
||||||
|
black, it will be changed to be only a 1% stop loss
|
||||||
|
|
||||||
|
This can be configured in the main configuration file and requires `"trailing_stop": true` to be set to true.
|
||||||
|
|
||||||
|
``` json
|
||||||
|
"trailing_stop_positive": 0.01,
|
||||||
|
```
|
||||||
|
|
||||||
|
The 0.01 would translate to a 1% stop loss, once you hit profit.
|
@ -180,7 +180,7 @@ class Analyze(object):
|
|||||||
:return: True if trade should be sold, False otherwise
|
:return: True if trade should be sold, False otherwise
|
||||||
"""
|
"""
|
||||||
current_profit = trade.calc_profit_percent(rate)
|
current_profit = trade.calc_profit_percent(rate)
|
||||||
if self.stop_loss_reached(current_profit=current_profit):
|
if self.stop_loss_reached(current_rate=rate, trade=trade, current_time=date):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
experimental = self.config.get('experimental', {})
|
experimental = self.config.get('experimental', {})
|
||||||
@ -204,12 +204,46 @@ class Analyze(object):
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def stop_loss_reached(self, current_profit: float) -> bool:
|
def stop_loss_reached(self, current_rate: float, trade: Trade, current_time: datetime) -> bool:
|
||||||
"""Based on current profit of the trade and configured stoploss, decides to sell or not"""
|
"""
|
||||||
|
Based on current profit of the trade and configured (trailing) stoploss,
|
||||||
|
decides to sell or not
|
||||||
|
"""
|
||||||
|
|
||||||
|
current_profit = trade.calc_profit_percent(current_rate)
|
||||||
|
trailing_stop = self.config.get('trailing_stop', False)
|
||||||
|
|
||||||
|
trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True)
|
||||||
|
|
||||||
|
# evaluate if the stoploss was hit
|
||||||
|
if self.strategy.stoploss is not None and trade.stop_loss >= current_rate:
|
||||||
|
|
||||||
|
if trailing_stop:
|
||||||
|
logger.debug(
|
||||||
|
f"HIT STOP: current price at {current_rate:.6f}, "
|
||||||
|
f"stop loss is {trade.stop_loss:.6f}, "
|
||||||
|
f"initial stop loss was at {trade.initial_stop_loss:.6f}, "
|
||||||
|
f"trade opened at {trade.open_rate:.6f}")
|
||||||
|
logger.debug(f"trailing stop saved {trade.stop_loss - trade.initial_stop_loss:.6f}")
|
||||||
|
|
||||||
if self.strategy.stoploss is not None and current_profit < self.strategy.stoploss:
|
|
||||||
logger.debug('Stop loss hit.')
|
logger.debug('Stop loss hit.')
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
# update the stop loss afterwards, after all by definition it's supposed to be hanging
|
||||||
|
if trailing_stop:
|
||||||
|
|
||||||
|
# check if we have a special stop loss for positive condition
|
||||||
|
# and if profit is positive
|
||||||
|
stop_loss_value = self.strategy.stoploss
|
||||||
|
if 'trailing_stop_positive' in self.config and current_profit > 0:
|
||||||
|
|
||||||
|
# Ignore mypy error check in configuration that this is a float
|
||||||
|
stop_loss_value = self.config.get('trailing_stop_positive') # type: ignore
|
||||||
|
logger.debug(f"using positive stop loss mode: {stop_loss_value} "
|
||||||
|
f"since we have profit {current_profit}")
|
||||||
|
|
||||||
|
trade.adjust_stop_loss(current_rate, stop_loss_value)
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def min_roi_reached(self, trade: Trade, current_profit: float, current_time: datetime) -> bool:
|
def min_roi_reached(self, trade: Trade, current_profit: float, current_time: datetime) -> bool:
|
||||||
|
@ -61,6 +61,8 @@ CONF_SCHEMA = {
|
|||||||
'minProperties': 1
|
'minProperties': 1
|
||||||
},
|
},
|
||||||
'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True},
|
'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True},
|
||||||
|
'trailing_stop': {'type': 'boolean'},
|
||||||
|
'trailing_stop_positive': {'type': 'number', 'minimum': 0, 'maximum': 1},
|
||||||
'unfilledtimeout': {
|
'unfilledtimeout': {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
'properties': {
|
'properties': {
|
||||||
|
@ -66,6 +66,10 @@ def has_column(columns, searchname: str) -> bool:
|
|||||||
return len(list(filter(lambda x: x["name"] == searchname, columns))) == 1
|
return len(list(filter(lambda x: x["name"] == searchname, columns))) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def get_column_def(columns, column: str, default: str) -> str:
|
||||||
|
return default if not has_column(columns, column) else column
|
||||||
|
|
||||||
|
|
||||||
def check_migrate(engine) -> None:
|
def check_migrate(engine) -> None:
|
||||||
"""
|
"""
|
||||||
Checks if migration is necessary and migrates if necessary
|
Checks if migration is necessary and migrates if necessary
|
||||||
@ -73,18 +77,32 @@ def check_migrate(engine) -> None:
|
|||||||
inspector = inspect(engine)
|
inspector = inspect(engine)
|
||||||
|
|
||||||
cols = inspector.get_columns('trades')
|
cols = inspector.get_columns('trades')
|
||||||
|
tabs = inspector.get_table_names()
|
||||||
|
table_back_name = 'trades_bak'
|
||||||
|
for i, table_back_name in enumerate(tabs):
|
||||||
|
table_back_name = f'trades_bak{i}'
|
||||||
|
logger.info(f'trying {table_back_name}')
|
||||||
|
|
||||||
|
# Check for latest column
|
||||||
|
if not has_column(cols, 'max_rate'):
|
||||||
|
open_rate_requested = get_column_def(cols, 'open_rate_requested', 'null')
|
||||||
|
close_rate_requested = get_column_def(cols, 'close_rate_requested', 'null')
|
||||||
|
stop_loss = get_column_def(cols, 'stop_loss', '0.0')
|
||||||
|
initial_stop_loss = get_column_def(cols, 'initial_stop_loss', '0.0')
|
||||||
|
max_rate = get_column_def(cols, 'max_rate', '0.0')
|
||||||
|
|
||||||
if not has_column(cols, 'fee_open'):
|
|
||||||
# Schema migration necessary
|
# Schema migration necessary
|
||||||
engine.execute("alter table trades rename to trades_bak")
|
engine.execute(f"alter table trades rename to {table_back_name}")
|
||||||
# let SQLAlchemy create the schema as required
|
# let SQLAlchemy create the schema as required
|
||||||
_DECL_BASE.metadata.create_all(engine)
|
_DECL_BASE.metadata.create_all(engine)
|
||||||
|
|
||||||
# Copy data back - following the correct schema
|
# Copy data back - following the correct schema
|
||||||
engine.execute("""insert into trades
|
engine.execute(f"""insert into trades
|
||||||
(id, exchange, pair, is_open, fee_open, fee_close, open_rate,
|
(id, exchange, pair, is_open, fee_open, fee_close, open_rate,
|
||||||
open_rate_requested, close_rate, close_rate_requested, close_profit,
|
open_rate_requested, close_rate, close_rate_requested, close_profit,
|
||||||
stake_amount, amount, open_date, close_date, open_order_id)
|
stake_amount, amount, open_date, close_date, open_order_id,
|
||||||
|
stop_loss, initial_stop_loss, max_rate
|
||||||
|
)
|
||||||
select id, lower(exchange),
|
select id, lower(exchange),
|
||||||
case
|
case
|
||||||
when instr(pair, '_') != 0 then
|
when instr(pair, '_') != 0 then
|
||||||
@ -94,21 +112,18 @@ def check_migrate(engine) -> None:
|
|||||||
end
|
end
|
||||||
pair,
|
pair,
|
||||||
is_open, fee fee_open, fee fee_close,
|
is_open, fee fee_open, fee fee_close,
|
||||||
open_rate, null open_rate_requested, close_rate,
|
open_rate, {open_rate_requested} open_rate_requested, close_rate,
|
||||||
null close_rate_requested, close_profit,
|
{close_rate_requested} close_rate_requested, close_profit,
|
||||||
stake_amount, amount, open_date, close_date, open_order_id
|
stake_amount, amount, open_date, close_date, open_order_id,
|
||||||
from trades_bak
|
{stop_loss} stop_loss, {initial_stop_loss} initial_stop_loss,
|
||||||
|
{max_rate} max_rate
|
||||||
|
from {table_back_name}
|
||||||
""")
|
""")
|
||||||
|
|
||||||
# Reread columns - the above recreated the table!
|
# Reread columns - the above recreated the table!
|
||||||
inspector = inspect(engine)
|
inspector = inspect(engine)
|
||||||
cols = inspector.get_columns('trades')
|
cols = inspector.get_columns('trades')
|
||||||
|
|
||||||
if not has_column(cols, 'open_rate_requested'):
|
|
||||||
engine.execute("alter table trades add open_rate_requested float")
|
|
||||||
if not has_column(cols, 'close_rate_requested'):
|
|
||||||
engine.execute("alter table trades add close_rate_requested float")
|
|
||||||
|
|
||||||
|
|
||||||
def cleanup() -> None:
|
def cleanup() -> None:
|
||||||
"""
|
"""
|
||||||
@ -151,6 +166,12 @@ class Trade(_DECL_BASE):
|
|||||||
open_date = Column(DateTime, nullable=False, default=datetime.utcnow)
|
open_date = Column(DateTime, nullable=False, default=datetime.utcnow)
|
||||||
close_date = Column(DateTime)
|
close_date = Column(DateTime)
|
||||||
open_order_id = Column(String)
|
open_order_id = Column(String)
|
||||||
|
# absolute value of the stop loss
|
||||||
|
stop_loss = Column(Float, nullable=True, default=0.0)
|
||||||
|
# absolute value of the initial stop loss
|
||||||
|
initial_stop_loss = Column(Float, nullable=True, default=0.0)
|
||||||
|
# absolute value of the highest reached price
|
||||||
|
max_rate = Column(Float, nullable=True, default=0.0)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return 'Trade(id={}, pair={}, amount={:.8f}, open_rate={:.8f}, open_since={})'.format(
|
return 'Trade(id={}, pair={}, amount={:.8f}, open_rate={:.8f}, open_since={})'.format(
|
||||||
@ -161,6 +182,45 @@ class Trade(_DECL_BASE):
|
|||||||
arrow.get(self.open_date).humanize() if self.is_open else 'closed'
|
arrow.get(self.open_date).humanize() if self.is_open else 'closed'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def adjust_stop_loss(self, current_price: float, stoploss: float, initial: bool = False):
|
||||||
|
"""this adjusts the stop loss to it's most recently observed setting"""
|
||||||
|
|
||||||
|
if initial and not (self.stop_loss is None or self.stop_loss == 0):
|
||||||
|
# Don't modify if called with initial and nothing to do
|
||||||
|
return
|
||||||
|
|
||||||
|
new_loss = float(current_price * (1 - abs(stoploss)))
|
||||||
|
|
||||||
|
# keeping track of the highest observed rate for this trade
|
||||||
|
if self.max_rate is None:
|
||||||
|
self.max_rate = current_price
|
||||||
|
else:
|
||||||
|
if current_price > self.max_rate:
|
||||||
|
self.max_rate = current_price
|
||||||
|
|
||||||
|
# no stop loss assigned yet
|
||||||
|
if not self.stop_loss:
|
||||||
|
logger.debug("assigning new stop loss")
|
||||||
|
self.stop_loss = new_loss
|
||||||
|
self.initial_stop_loss = new_loss
|
||||||
|
|
||||||
|
# evaluate if the stop loss needs to be updated
|
||||||
|
else:
|
||||||
|
if new_loss > self.stop_loss: # stop losses only walk up, never down!
|
||||||
|
self.stop_loss = new_loss
|
||||||
|
logger.debug("adjusted stop loss")
|
||||||
|
else:
|
||||||
|
logger.debug("keeping current stop loss")
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
f"{self.pair} - current price {current_price:.8f}, "
|
||||||
|
f"bought at {self.open_rate:.8f} and calculated "
|
||||||
|
f"stop loss is at: {self.initial_stop_loss:.8f} initial "
|
||||||
|
f"stop at {self.stop_loss:.8f}. "
|
||||||
|
f"trailing stop loss saved us: "
|
||||||
|
f"{float(self.stop_loss) - float(self.initial_stop_loss):.8f} "
|
||||||
|
f"and max observed rate was {self.max_rate:.8f}")
|
||||||
|
|
||||||
def update(self, order: Dict) -> None:
|
def update(self, order: Dict) -> None:
|
||||||
"""
|
"""
|
||||||
Updates this entity with amount and actual open/close rates.
|
Updates this entity with amount and actual open/close rates.
|
||||||
|
@ -42,6 +42,7 @@ def test_analyze_object() -> None:
|
|||||||
assert hasattr(Analyze, 'get_signal')
|
assert hasattr(Analyze, 'get_signal')
|
||||||
assert hasattr(Analyze, 'should_sell')
|
assert hasattr(Analyze, 'should_sell')
|
||||||
assert hasattr(Analyze, 'min_roi_reached')
|
assert hasattr(Analyze, 'min_roi_reached')
|
||||||
|
assert hasattr(Analyze, 'stop_loss_reached')
|
||||||
|
|
||||||
|
|
||||||
def test_dataframe_correct_length(result):
|
def test_dataframe_correct_length(result):
|
||||||
|
@ -1684,6 +1684,103 @@ def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, fee, markets, m
|
|||||||
assert freqtrade.handle_trade(trade) is True
|
assert freqtrade.handle_trade(trade) is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_trailing_stop_loss(default_conf, limit_buy_order, fee, caplog, mocker) -> None:
|
||||||
|
"""
|
||||||
|
Test sell_profit_only feature when enabled and we have a loss
|
||||||
|
"""
|
||||||
|
patch_get_signal(mocker)
|
||||||
|
patch_RPCManager(mocker)
|
||||||
|
patch_coinmarketcap(mocker)
|
||||||
|
mocker.patch('freqtrade.freqtradebot.Analyze.min_roi_reached', return_value=False)
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
validate_pairs=MagicMock(),
|
||||||
|
get_ticker=MagicMock(return_value={
|
||||||
|
'bid': 0.00000102,
|
||||||
|
'ask': 0.00000103,
|
||||||
|
'last': 0.00000102
|
||||||
|
}),
|
||||||
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
|
get_fee=fee,
|
||||||
|
)
|
||||||
|
|
||||||
|
conf = deepcopy(default_conf)
|
||||||
|
conf['trailing_stop'] = True
|
||||||
|
print(limit_buy_order)
|
||||||
|
freqtrade = FreqtradeBot(conf)
|
||||||
|
freqtrade.create_trade()
|
||||||
|
|
||||||
|
trade = Trade.query.first()
|
||||||
|
trade.update(limit_buy_order)
|
||||||
|
caplog.set_level(logging.DEBUG)
|
||||||
|
# Sell as trailing-stop is reached
|
||||||
|
assert freqtrade.handle_trade(trade) is True
|
||||||
|
assert log_has(
|
||||||
|
f'HIT STOP: current price at 0.000001, stop loss is {trade.stop_loss:.6f}, '
|
||||||
|
f'initial stop loss was at 0.000010, trade opened at 0.000011', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee, caplog, mocker) -> None:
|
||||||
|
"""
|
||||||
|
Test sell_profit_only feature when enabled and we have a loss
|
||||||
|
"""
|
||||||
|
buy_price = limit_buy_order['price']
|
||||||
|
patch_get_signal(mocker)
|
||||||
|
patch_RPCManager(mocker)
|
||||||
|
patch_coinmarketcap(mocker)
|
||||||
|
mocker.patch('freqtrade.freqtradebot.Analyze.min_roi_reached', return_value=False)
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
validate_pairs=MagicMock(),
|
||||||
|
get_ticker=MagicMock(return_value={
|
||||||
|
'bid': buy_price - 0.000001,
|
||||||
|
'ask': buy_price - 0.000001,
|
||||||
|
'last': buy_price - 0.000001
|
||||||
|
}),
|
||||||
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
|
get_fee=fee,
|
||||||
|
)
|
||||||
|
|
||||||
|
conf = deepcopy(default_conf)
|
||||||
|
conf['trailing_stop'] = True
|
||||||
|
conf['trailing_stop_positive'] = 0.01
|
||||||
|
freqtrade = FreqtradeBot(conf)
|
||||||
|
freqtrade.create_trade()
|
||||||
|
|
||||||
|
trade = Trade.query.first()
|
||||||
|
trade.update(limit_buy_order)
|
||||||
|
caplog.set_level(logging.DEBUG)
|
||||||
|
# stop-loss not reached
|
||||||
|
assert freqtrade.handle_trade(trade) is False
|
||||||
|
|
||||||
|
# Raise ticker above buy price
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_ticker',
|
||||||
|
MagicMock(return_value={
|
||||||
|
'bid': buy_price + 0.000003,
|
||||||
|
'ask': buy_price + 0.000003,
|
||||||
|
'last': buy_price + 0.000003
|
||||||
|
}))
|
||||||
|
# stop-loss not reached, adjusted stoploss
|
||||||
|
assert freqtrade.handle_trade(trade) is False
|
||||||
|
assert log_has(f'using positive stop loss mode: 0.01 since we have profit 0.26662643',
|
||||||
|
caplog.record_tuples)
|
||||||
|
assert log_has(f'adjusted stop loss', caplog.record_tuples)
|
||||||
|
assert trade.stop_loss == 0.0000138501
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_ticker',
|
||||||
|
MagicMock(return_value={
|
||||||
|
'bid': buy_price + 0.000002,
|
||||||
|
'ask': buy_price + 0.000002,
|
||||||
|
'last': buy_price + 0.000002
|
||||||
|
}))
|
||||||
|
# Lower price again (but still positive)
|
||||||
|
assert freqtrade.handle_trade(trade) is True
|
||||||
|
assert log_has(
|
||||||
|
f'HIT STOP: current price at {buy_price + 0.000002:.6f}, '
|
||||||
|
f'stop loss is {trade.stop_loss:.6f}, '
|
||||||
|
f'initial stop loss was at 0.000010, trade opened at 0.000011', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order,
|
def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order,
|
||||||
fee, markets, mocker) -> None:
|
fee, markets, mocker) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -7,6 +7,7 @@ from sqlalchemy import create_engine
|
|||||||
|
|
||||||
from freqtrade import constants, OperationalException
|
from freqtrade import constants, OperationalException
|
||||||
from freqtrade.persistence import Trade, init, clean_dry_run_db
|
from freqtrade.persistence import Trade, init, clean_dry_run_db
|
||||||
|
from freqtrade.tests.conftest import log_has
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='function')
|
@pytest.fixture(scope='function')
|
||||||
@ -400,9 +401,12 @@ def test_migrate_old(mocker, default_conf, fee):
|
|||||||
assert trade.stake_amount == default_conf.get("stake_amount")
|
assert trade.stake_amount == default_conf.get("stake_amount")
|
||||||
assert trade.pair == "ETC/BTC"
|
assert trade.pair == "ETC/BTC"
|
||||||
assert trade.exchange == "bittrex"
|
assert trade.exchange == "bittrex"
|
||||||
|
assert trade.max_rate == 0.0
|
||||||
|
assert trade.stop_loss == 0.0
|
||||||
|
assert trade.initial_stop_loss == 0.0
|
||||||
|
|
||||||
|
|
||||||
def test_migrate_new(mocker, default_conf, fee):
|
def test_migrate_new(mocker, default_conf, fee, caplog):
|
||||||
"""
|
"""
|
||||||
Test Database migration (starting with new pairformat)
|
Test Database migration (starting with new pairformat)
|
||||||
"""
|
"""
|
||||||
@ -439,6 +443,11 @@ def test_migrate_new(mocker, default_conf, fee):
|
|||||||
# Create table using the old format
|
# Create table using the old format
|
||||||
engine.execute(create_table_old)
|
engine.execute(create_table_old)
|
||||||
engine.execute(insert_table_old)
|
engine.execute(insert_table_old)
|
||||||
|
|
||||||
|
# fake previous backup
|
||||||
|
engine.execute("create table trades_bak as select * from trades")
|
||||||
|
|
||||||
|
engine.execute("create table trades_bak1 as select * from trades")
|
||||||
# Run init to test migration
|
# Run init to test migration
|
||||||
init(default_conf)
|
init(default_conf)
|
||||||
|
|
||||||
@ -453,3 +462,54 @@ def test_migrate_new(mocker, default_conf, fee):
|
|||||||
assert trade.stake_amount == default_conf.get("stake_amount")
|
assert trade.stake_amount == default_conf.get("stake_amount")
|
||||||
assert trade.pair == "ETC/BTC"
|
assert trade.pair == "ETC/BTC"
|
||||||
assert trade.exchange == "binance"
|
assert trade.exchange == "binance"
|
||||||
|
assert trade.max_rate == 0.0
|
||||||
|
assert trade.stop_loss == 0.0
|
||||||
|
assert trade.initial_stop_loss == 0.0
|
||||||
|
assert log_has("trying trades_bak1", caplog.record_tuples)
|
||||||
|
assert log_has("trying trades_bak2", caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
def test_adjust_stop_loss(limit_buy_order, limit_sell_order, fee):
|
||||||
|
trade = Trade(
|
||||||
|
pair='ETH/BTC',
|
||||||
|
stake_amount=0.001,
|
||||||
|
fee_open=fee.return_value,
|
||||||
|
fee_close=fee.return_value,
|
||||||
|
exchange='bittrex',
|
||||||
|
open_rate=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
trade.adjust_stop_loss(trade.open_rate, 0.05, True)
|
||||||
|
assert trade.stop_loss == 0.95
|
||||||
|
assert trade.max_rate == 1
|
||||||
|
assert trade.initial_stop_loss == 0.95
|
||||||
|
|
||||||
|
# Get percent of profit with a lowre rate
|
||||||
|
trade.adjust_stop_loss(0.96, 0.05)
|
||||||
|
assert trade.stop_loss == 0.95
|
||||||
|
assert trade.max_rate == 1
|
||||||
|
assert trade.initial_stop_loss == 0.95
|
||||||
|
|
||||||
|
# Get percent of profit with a custom rate (Higher than open rate)
|
||||||
|
trade.adjust_stop_loss(1.3, -0.1)
|
||||||
|
assert round(trade.stop_loss, 8) == 1.17
|
||||||
|
assert trade.max_rate == 1.3
|
||||||
|
assert trade.initial_stop_loss == 0.95
|
||||||
|
|
||||||
|
# current rate lower again ... should not change
|
||||||
|
trade.adjust_stop_loss(1.2, 0.1)
|
||||||
|
assert round(trade.stop_loss, 8) == 1.17
|
||||||
|
assert trade.max_rate == 1.3
|
||||||
|
assert trade.initial_stop_loss == 0.95
|
||||||
|
|
||||||
|
# current rate higher... should raise stoploss
|
||||||
|
trade.adjust_stop_loss(1.4, 0.1)
|
||||||
|
assert round(trade.stop_loss, 8) == 1.26
|
||||||
|
assert trade.max_rate == 1.4
|
||||||
|
assert trade.initial_stop_loss == 0.95
|
||||||
|
|
||||||
|
# Initial is true but stop_loss set - so doesn't do anything
|
||||||
|
trade.adjust_stop_loss(1.7, 0.1, True)
|
||||||
|
assert round(trade.stop_loss, 8) == 1.26
|
||||||
|
assert trade.max_rate == 1.4
|
||||||
|
assert trade.initial_stop_loss == 0.95
|
||||||
|
Loading…
Reference in New Issue
Block a user