Merge pull request #963 from freqtrade/feat/stop_loss

Feat/stop loss
This commit is contained in:
Michael Egger 2018-07-01 20:50:13 +02:00 committed by GitHub
commit 5e4a6ba7ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 358 additions and 25 deletions

View File

@ -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

View File

@ -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,

View File

@ -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).

View File

@ -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
View 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.

View File

@ -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:

View File

@ -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': {

View File

@ -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.

View File

@ -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):

View File

@ -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:
""" """

View File

@ -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