Merge branch 'develop' of https://github.com/freqtrade/freqtrade into develop

This commit is contained in:
AxelCh 2019-01-23 13:26:05 -04:00
commit 06e0616fb0
23 changed files with 462 additions and 91 deletions

View File

@ -19,7 +19,7 @@ addons:
install:
- cd build_helpers && ./install_ta-lib.sh; cd ..
- export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
- pip install --upgrade flake8 coveralls pytest-random-order pytest-asyncio mypy
- pip install --upgrade pytest-random-order
- pip install -r requirements-dev.txt
- pip install -e .
jobs:

View File

@ -14,6 +14,10 @@ Few pointers for contributions:
If you are unsure, discuss the feature on our [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE)
or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a PR.
## Getting started
Best start by reading the [documentation](https://www.freqtrade.io/) to get a feel for what is possible with the bot, or head straight to the [Developer-documentation](https://www.freqtrade.io/en/latest/developer/) (WIP) which should help you getting started.
## Before sending the PR:
### 1. Run unit tests
@ -41,12 +45,6 @@ pytest freqtrade/tests/test_<file_name>.py::test_<method_name>
### 2. Test if your code is PEP8 compliant
#### Install packages
```bash
pip3.6 install flake8 coveralls
```
#### Run Flake8
```bash
@ -60,22 +58,12 @@ Guide for installing them is [here](http://flake8.pycqa.org/en/latest/user/using
### 3. Test if all type-hints are correct
#### Install packages
``` bash
pip3.6 install mypy
```
#### Run mypy
``` bash
mypy freqtrade
```
## Getting started
Best start by reading the [documentation](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md) to get a feel for what is possible with the bot, or head straight to the [Developer-documentation](https://github.com/freqtrade/freqtrade/blob/develop/docs/developer.md) (WIP) which should help you getting started.
## (Core)-Committer Guide
### Process: Pull Requests

View File

@ -37,7 +37,8 @@
"buy": "limit",
"sell": "limit",
"stoploss": "market",
"stoploss_on_exchange": "false"
"stoploss_on_exchange": "false",
"stoploss_on_exchange_interval": 60
},
"order_time_in_force": {
"buy": "gtc",

View File

@ -216,12 +216,40 @@ This is the set of candles the bot should download and use for the analysis.
Common values are `"1m"`, `"5m"`, `"15m"`, `"1h"`, however all values supported by your exchange should work.
Please note that the same buy/sell signals may work with one interval, but not the other.
This setting is accessible within the strategy by using `self.ticker_interval`.
### Metadata dict
The metadata-dict (available for `populate_buy_trend`, `populate_sell_trend`, `populate_indicators`) contains additional information.
Currently this is `pair`, which can be accessed using `metadata['pair']` - and will return a pair in the format `XRP/BTC`.
The Metadata-dict should not be modified and does not persist information across multiple calls.
Instead, have a look at the section [Storing information](#Storing-information)
### Storing information
Storing information can be accomplished by crating a new dictionary within the strategy class.
The name of the variable can be choosen at will, but should be prefixed with `cust_` to avoid naming collisions with predefined strategy variables.
```python
class Awesomestrategy(IStrategy):
# Create custom dictionary
cust_info = {}
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# Check if the entry already exists
if "crosstime" in self.cust_info[metadata["pair"]:
self.cust_info[metadata["pair"]["crosstime"] += 1
else:
self.cust_info[metadata["pair"]["crosstime"] = 1
```
!!! Warning:
The data is not persisted after a bot-restart (or config-reload). Also, the amount of data should be kept smallish (no DataFrames and such), otherwise the bot will start to consume a lot of memory and eventually run out of memory and crash.
!!! Note:
If the data is pair-specific, make sure to use pair as one of the keys in the dictionary.
### Where is the default strategy?
The default buy strategy is located in the file

View File

@ -13,7 +13,7 @@ The table below will list all configuration parameters.
|----------|---------|-----------|-------------|
| `max_open_trades` | 3 | Yes | Number of trades open your bot will have. If -1 then it is ignored (i.e. potentially unlimited open trades)
| `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. Set it to 'unlimited' to allow the bot to use all avaliable balance.
| `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. Set it to `"unlimited"` to allow the bot to use all avaliable balance.
| `ticker_interval` | [1m, 5m, 30m, 1h, 1d] | No | The ticker interval to use (1min, 5 min, 30 min, 1 hour or 1 day). Default 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.
@ -144,10 +144,10 @@ end up paying more then would probably have been necessary.
### Understand order_types
`order_types` contains a dict mapping order-types to market-types as well as stoploss on or off exchange type. This allows to buy using limit orders, sell using limit-orders, and create stoploss orders using market. It also allows to set the stoploss "on exchange" which means stoploss order would be placed immediately once the buy order is fulfilled.
`order_types` contains a dict mapping order-types to market-types as well as stoploss on or off exchange type and stoploss on exchange update interval in seconds. This allows to buy using limit orders, sell using limit-orders, and create stoploss orders using market. It also allows to set the stoploss "on exchange" which means stoploss order would be placed immediately once the buy order is fulfilled. In case stoploss on exchange and `trailing_stop` are both set, then the bot will use `stoploss_on_exchange_interval` to check it periodically and update it if necessary (e.x. in case of trailing stoploss).
This can be set in the configuration or in the strategy. Configuration overwrites strategy configurations.
If this is configured, all 4 values (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`) need to be present, otherwise the bot warn about it and will fail to start.
If this is configured, all 4 values (`"buy"`, `"sell"`, `"stoploss"` and `"stoploss_on_exchange"`) need to be present, otherwise the bot warn about it and will fail to start.
The below is the default which is used if this is not configured in either Strategy or configuration.
```python
@ -155,7 +155,8 @@ The below is the default which is used if this is not configured in either Strat
"buy": "limit",
"sell": "limit",
"stoploss": "market",
"stoploss_on_exchange": False
"stoploss_on_exchange": False,
"stoploss_on_exchange_interval": 60
},
```
@ -163,6 +164,9 @@ The below is the default which is used if this is not configured in either Strat
Not all exchanges support "market" orders.
The following message will be shown if your exchange does not support market orders: `"Exchange <yourexchange> does not support market orders."`
!!! Note
stoploss on exchange interval is not mandatory. Do not change it's value if you are unsure of what you are doing. For more information about how stoploss works please read [the stoploss documentation](stoploss.md).
### Understand order_time_in_force
`order_time_in_force` defines the policy by which the order is executed on the exchange. Three commonly used time in force are:<br/>
**GTC (Goog Till Canceled):**

View File

@ -4,8 +4,20 @@ This page is intended for developers of FreqTrade, people who want to contribute
All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome. We [track issues](https://github.com/freqtrade/freqtrade/issues) on [GitHub](https://github.com) and also have a dev channel in [slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE) where you can ask questions.
## Documentation
## Module
Documentation is available at [https://freqtrade.io](https://www.freqtrade.io/) and needs to be provided with every new feature PR.
Special fields for the documentation (like Note boxes, ...) can be found [here](https://squidfunk.github.io/mkdocs-material/extensions/admonition/).
## Developer setup
To configure a development environment, use best use the `setup.sh` script and answer "y" when asked "Do you want to install dependencies for dev [y/N]? ".
Alternatively (if your system is not supported by the setup.sh script), follow the manual installation process and run `pip3 install -r requirements-dev.txt`.
This will install all required tools for development, including `pytest`, `flake8`, `mypy`, and `coveralls`.
## Modules
### Dynamic Pairlist

View File

@ -24,7 +24,7 @@ The answer comes to two factors:
Means over X trades what is the percentage of winning trades to total number of trades (note that we don't consider how much you gained but only If you won or not).
`W = (Number of winning trades) / (Number of losing trades)`
`W = (Number of winning trades) / (Total number of trades)`
### Risk Reward Ratio
Risk Reward Ratio is a formula used to measure the expected gains of a given investment against the risk of loss. It is basically what you potentially win divided by what you potentially lose:
@ -209,4 +209,4 @@ The full timerange specification:
* Use tickframes till 2018/01/31: --timerange=-20180131
* Use tickframes since 2018/01/31: --timerange=20180131-
* Use tickframes since 2018/01/31 till 2018/03/01 : --timerange=20180131-20180301
* Use tickframes between POSIX timestamps 1527595200 1527618600: --timerange=1527595200-1527618600
* Use tickframes between POSIX timestamps 1527595200 1527618600: --timerange=1527595200-1527618600

View File

@ -2,12 +2,20 @@
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
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.
!!! Note
All stoploss properties can be configured in eihter Strategy or configuration. Configuration values override strategy values.
All stoploss properties can be configured in either Strategy or configuration. Configuration values override strategy values.
Those stoploss modes can be *on exchange* or *off exchange*. If the stoploss is *on exchange* it means a stoploss limit order is placed on the exchange immediately after buy order happens successfuly. This will protect you against sudden crashes in market as the order will be in the queue immediately and if market goes down then the order has more chance of being fulfilled.
In case of stoploss on exchange there is another parameter called `stoploss_on_exchange_interval`. This configures the interval in seconds at which the bot will check the stoploss and update it if necessary. As an example in case of trailing stoploss if the order is on the exchange and the market is going up then the bot automatically cancels the previous stoploss order and put a new one with a stop value higher than previous one. It is clear that the bot cannot do it every 5 seconds otherwise it gets banned. So this parameter will tell the bot how often it should update the stoploss order. The default value is 60 (1 minute).
!!! Note
Stoploss on exchange is only supported for Binance as of now.
## Static Stop Loss

View File

@ -112,7 +112,8 @@ CONF_SCHEMA = {
'buy': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
'sell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
'stoploss_on_exchange': {'type': 'boolean'}
'stoploss_on_exchange': {'type': 'boolean'},
'stoploss_on_exchange_interval': {'type': 'number'}
},
'required': ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']
},
@ -137,7 +138,7 @@ CONF_SCHEMA = {
'pairlist': {
'type': 'object',
'properties': {
'method': {'type': 'string', 'enum': AVAILABLE_PAIRLISTS},
'method': {'type': 'string', 'enum': AVAILABLE_PAIRLISTS},
'config': {'type': 'object'}
},
'required': ['method']

View File

@ -402,8 +402,11 @@ class Exchange(object):
return self._dry_run_open_orders[order_id]
try:
return self._api.create_order(pair, 'stop_loss_limit', 'sell',
amount, rate, {'stopPrice': stop_price})
order = self._api.create_order(pair, 'stop_loss_limit', 'sell',
amount, rate, {'stopPrice': stop_price})
logger.info('stoploss limit order added for %s. '
'stop price: %s. limit: %s' % (pair, stop_price, rate))
return order
except ccxt.InsufficientFunds as e:
raise DependencyException(
@ -553,12 +556,17 @@ class Exchange(object):
tickers = await asyncio.gather(*input_coroutines, return_exceptions=True)
# handle caching
for pair, ticks in tickers:
for res in tickers:
if isinstance(res, Exception):
logger.warning("Async code raised an exception: %s", res.__class__.__name__)
continue
pair = res[0]
ticks = res[1]
# keeping last candle time as last refreshed time of the pair
if ticks:
self._pairs_last_refresh_time[pair] = ticks[-1][0] // 1000
# keeping parsed dataframe in cache
self._klines[pair] = parse_ticker_dataframe(ticks, tick_interval, fill_missing=True)
# keeping parsed dataframe in cache
self._klines[pair] = parse_ticker_dataframe(ticks, tick_interval, fill_missing=True)
return tickers
@retrier_async
@ -575,9 +583,12 @@ class Exchange(object):
# Ex: Bittrex returns a list of tickers ASC (oldest first, newest last)
# when GDAX returns a list of tickers DESC (newest first, oldest last)
# Only sort if necessary to save computing time
if data and data[0][0] > data[-1][0]:
data = sorted(data, key=lambda x: x[0])
try:
if data and data[0][0] > data[-1][0]:
data = sorted(data, key=lambda x: x[0])
except IndexError:
logger.exception("Error loading %s. Result was %s.", pair, data)
return pair, []
logger.debug("done fetching %s ...", pair)
return pair, data

View File

@ -34,7 +34,7 @@ class FreqtradeBot(object):
This is from here the bot start its logic.
"""
def __init__(self, config: Dict[str, Any])-> None:
def __init__(self, config: Dict[str, Any]) -> None:
"""
Init all variables and object the bot need to work
:param config: configuration dict, you can use the Configuration.get_config()
@ -601,7 +601,7 @@ class FreqtradeBot(object):
if self.check_sell(trade, sell_rate, buy, sell):
return True
break
else:
logger.debug('checking sell')
if self.check_sell(trade, sell_rate, buy, sell):
@ -613,7 +613,7 @@ class FreqtradeBot(object):
def handle_stoploss_on_exchange(self, trade: Trade) -> bool:
"""
Check if trade is fulfilled in which case the stoploss
on exchange should be added immediately if stoploss on exchnage
on exchange should be added immediately if stoploss on exchange
is enabled.
"""
@ -630,13 +630,14 @@ class FreqtradeBot(object):
stop_price = trade.open_rate * (1 + stoploss)
# limit price should be less than stop price.
# 0.98 is arbitrary here.
limit_price = stop_price * 0.98
# 0.99 is arbitrary here.
limit_price = stop_price * 0.99
stoploss_order_id = self.exchange.stoploss_limit(
pair=trade.pair, amount=trade.amount, stop_price=stop_price, rate=limit_price
)['id']
trade.stoploss_order_id = str(stoploss_order_id)
trade.stoploss_last_update = datetime.now()
# Or the trade open and there is already a stoploss on exchange.
# so we check if it is hit ...
@ -647,10 +648,38 @@ class FreqtradeBot(object):
trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value
trade.update(order)
result = True
else:
result = False
elif self.config.get('trailing_stop', False):
# if trailing stoploss is enabled we check if stoploss value has changed
# in which case we cancel stoploss order and put another one with new
# value immediately
self.handle_trailing_stoploss_on_exchange(trade, order)
return result
def handle_trailing_stoploss_on_exchange(self, trade: Trade, order):
"""
Check to see if stoploss on exchange should be updated
in case of trailing stoploss on exchange
:param Trade: Corresponding Trade
:param order: Current on exchange stoploss order
:return: None
"""
if trade.stop_loss > float(order['info']['stopPrice']):
# we check if the update is neccesary
update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60)
if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() > update_beat:
# cancelling the current stoploss on exchange first
logger.info('Trailing stoploss: cancelling current stoploss on exchange '
'in order to add another one ...')
if self.exchange.cancel_order(order['id'], trade.pair):
# creating the new one
stoploss_order_id = self.exchange.stoploss_limit(
pair=trade.pair, amount=trade.amount,
stop_price=trade.stop_loss, rate=trade.stop_loss * 0.99
)['id']
trade.stoploss_order_id = str(stoploss_order_id)
def check_sell(self, trade: Trade, sell_rate: float, buy: bool, sell: bool) -> bool:
if self.edge:
stoploss = self.edge.stoploss(trade.pair)

View File

@ -13,7 +13,7 @@ from typing import Any, Dict, List, NamedTuple, Optional
from pandas import DataFrame
from tabulate import tabulate
import freqtrade.optimize as optimize
from freqtrade import optimize
from freqtrade import DependencyException, constants
from freqtrade.arguments import Arguments
from freqtrade.configuration import Configuration
@ -134,7 +134,9 @@ class Backtesting(object):
len(results[results.profit_abs > 0]),
len(results[results.profit_abs < 0])
])
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="pipe")
# Ignore type as floatfmt does allow tuples but mypy does not know that
return tabulate(tabular_data, headers=headers, # type: ignore
floatfmt=floatfmt, tablefmt="pipe")
def _generate_text_table_sell_reason(self, data: Dict[str, Dict], results: DataFrame) -> str:
"""
@ -168,7 +170,9 @@ class Backtesting(object):
len(results[results.profit_abs > 0]),
len(results[results.profit_abs < 0])
])
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="pipe")
# Ignore type as floatfmt does allow tuples but mypy does not know that
return tabulate(tabular_data, headers=headers, # type: ignore
floatfmt=floatfmt, tablefmt="pipe")
def _store_backtest_result(self, recordfilename: str, results: DataFrame,
strategyname: Optional[str] = None) -> None:
@ -221,7 +225,7 @@ class Backtesting(object):
elif sell.sell_type == (SellType.ROI):
# get next entry in min_roi > to trade duration
# Interface.py skips on trade_duration <= duration
roi_entry = max(list(filter(lambda x: trade_dur > x,
roi_entry = max(list(filter(lambda x: trade_dur >= x,
self.strategy.minimal_roi.keys())))
roi = self.strategy.minimal_roi[roi_entry]

View File

@ -67,7 +67,9 @@ class EdgeCli(object):
round(result[1].avg_trade_duration)
])
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="pipe")
# Ignore type as floatfmt does allow tuples but mypy does not know that
return tabulate(tabular_data, headers=headers, # type: ignore
floatfmt=floatfmt, tablefmt="pipe")
def start(self) -> None:
self.edge.calculate()

View File

@ -83,7 +83,7 @@ def check_migrate(engine) -> None:
logger.debug(f'trying {table_back_name}')
# Check for latest column
if not has_column(cols, 'stoploss_order_id'):
if not has_column(cols, 'stoploss_last_update'):
logger.info(f'Running database migration - backup available as {table_back_name}')
fee_open = get_column_def(cols, 'fee_open', 'fee')
@ -93,6 +93,7 @@ def check_migrate(engine) -> None:
stop_loss = get_column_def(cols, 'stop_loss', '0.0')
initial_stop_loss = get_column_def(cols, 'initial_stop_loss', '0.0')
stoploss_order_id = get_column_def(cols, 'stoploss_order_id', 'null')
stoploss_last_update = get_column_def(cols, 'stoploss_last_update', 'null')
max_rate = get_column_def(cols, 'max_rate', '0.0')
sell_reason = get_column_def(cols, 'sell_reason', 'null')
strategy = get_column_def(cols, 'strategy', 'null')
@ -111,7 +112,8 @@ def check_migrate(engine) -> None:
(id, exchange, pair, is_open, fee_open, fee_close, open_rate,
open_rate_requested, close_rate, close_rate_requested, close_profit,
stake_amount, amount, open_date, close_date, open_order_id,
stop_loss, initial_stop_loss, stoploss_order_id, max_rate, sell_reason, strategy,
stop_loss, initial_stop_loss, stoploss_order_id, stoploss_last_update,
max_rate, sell_reason, strategy,
ticker_interval
)
select id, lower(exchange),
@ -127,9 +129,9 @@ def check_migrate(engine) -> None:
{close_rate_requested} close_rate_requested, close_profit,
stake_amount, amount, open_date, close_date, open_order_id,
{stop_loss} stop_loss, {initial_stop_loss} initial_stop_loss,
{stoploss_order_id} stoploss_order_id, {max_rate} max_rate,
{sell_reason} sell_reason, {strategy} strategy,
{ticker_interval} ticker_interval
{stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update,
{max_rate} max_rate, {sell_reason} sell_reason,
{strategy} strategy, {ticker_interval} ticker_interval
from {table_back_name}
""")
@ -185,6 +187,8 @@ class Trade(_DECL_BASE):
initial_stop_loss = Column(Float, nullable=True, default=0.0)
# stoploss order id which is on exchange
stoploss_order_id = Column(String, nullable=True, index=True)
# last update time of the stoploss order on exchange
stoploss_last_update = Column(DateTime, nullable=True)
# absolute value of the highest reached price
max_rate = Column(Float, nullable=True, default=0.0)
sell_reason = Column(String, nullable=True)
@ -218,11 +222,13 @@ class Trade(_DECL_BASE):
logger.debug("assigning new stop loss")
self.stop_loss = new_loss
self.initial_stop_loss = new_loss
self.stoploss_last_update = datetime.utcnow()
# 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
self.stoploss_last_update = datetime.utcnow()
logger.debug("adjusted stop loss")
else:
logger.debug("keeping current stop loss")

View File

@ -246,14 +246,14 @@ class Telegram(RPC):
stake_cur,
fiat_disp_cur
)
stats = tabulate(stats,
headers=[
'Day',
f'Profit {stake_cur}',
f'Profit {fiat_disp_cur}'
],
tablefmt='simple')
message = f'<b>Daily Profit over the last {timescale} days</b>:\n<pre>{stats}</pre>'
stats_tab = tabulate(stats,
headers=[
'Day',
f'Profit {stake_cur}',
f'Profit {fiat_disp_cur}'
],
tablefmt='simple')
message = f'<b>Daily Profit over the last {timescale} days</b>:\n<pre>{stats_tab}</pre>'
self._send_msg(message, bot=bot, parse_mode=ParseMode.HTML)
except RPCException as e:
self._send_msg(str(e), bot=bot)

View File

@ -80,7 +80,8 @@ class IStrategy(ABC):
'buy': 'limit',
'sell': 'limit',
'stoploss': 'limit',
'stoploss_on_exchange': False
'stoploss_on_exchange': False,
'stoploss_on_exchange_interval': 60,
}
# Optional time in force
@ -233,12 +234,9 @@ class IStrategy(ABC):
current_rate = low or rate
current_profit = trade.calc_profit_percent(current_rate)
if self.order_types.get('stoploss_on_exchange'):
stoplossflag = SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)
else:
stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade,
current_time=date, current_profit=current_profit,
force_stoploss=force_stoploss)
stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade,
current_time=date, current_profit=current_profit,
force_stoploss=force_stoploss)
if stoplossflag.sell_flag:
return stoplossflag
@ -276,12 +274,13 @@ class IStrategy(ABC):
"""
trailing_stop = self.config.get('trailing_stop', False)
trade.adjust_stop_loss(trade.open_rate, force_stoploss if force_stoploss
else self.stoploss, initial=True)
# evaluate if the stoploss was hit
if self.stoploss is not None and trade.stop_loss >= current_rate:
# evaluate if the stoploss was hit if stoploss is not on exchange
if ((self.stoploss is not None) and
(trade.stop_loss >= current_rate) and
(not self.order_types.get('stoploss_on_exchange'))):
selltype = SellType.STOP_LOSS
# If Trailing stop (and max-rate did move above open rate)
if trailing_stop and trade.open_rate != trade.max_rate:
@ -301,7 +300,8 @@ class IStrategy(ABC):
# check if we have a special stop loss for positive condition
# and if profit is positive
stop_loss_value = self.stoploss
stop_loss_value = force_stoploss if force_stoploss else self.stoploss
sl_offset = self.config.get('trailing_stop_positive_offset', 0.0)
if 'trailing_stop_positive' in self.config and current_profit > sl_offset:
@ -319,17 +319,18 @@ class IStrategy(ABC):
def min_roi_reached(self, trade: Trade, current_profit: float, current_time: datetime) -> bool:
"""
Based an earlier trade and current price and ROI configuration, decides whether bot should
sell
sell. Requires current_profit to be in percent!!
:return True if bot should sell at current rate
"""
# Check if time matches and current rate is above threshold
time_diff = (current_time.timestamp() - trade.open_date.timestamp()) / 60
for duration, threshold in self.minimal_roi.items():
if time_diff <= duration:
continue
if current_profit > threshold:
return True
trade_dur = (current_time.timestamp() - trade.open_date.timestamp()) / 60
# Get highest entry in ROI dict where key >= trade-duration
roi_entry = max(list(filter(lambda x: trade_dur >= x, self.minimal_roi.keys())))
threshold = self.minimal_roi[roi_entry]
if current_profit > threshold:
return True
return False

View File

@ -923,6 +923,30 @@ async def test_async_get_candles_history(default_conf, mocker):
assert exchange._async_get_candle_history.call_count == 2
@pytest.mark.asyncio
async def test_async_get_candles_history_inv_result(default_conf, mocker, caplog):
async def mock_get_candle_hist(pair, *args, **kwargs):
if pair == 'ETH/BTC':
return [[]]
else:
raise TypeError()
exchange = get_patched_exchange(mocker, default_conf)
# Monkey-patch async function with empty result
exchange._api_async.fetch_ohlcv = MagicMock(side_effect=mock_get_candle_hist)
pairs = ['ETH/BTC', 'XRP/BTC']
res = await exchange.async_get_candles_history(pairs, "5m")
assert type(res) is list
assert len(res) == 2
assert type(res[0]) is tuple
assert type(res[1]) is TypeError
assert log_has("Error loading ETH/BTC. Result was [[]].", caplog.record_tuples)
assert log_has("Async code raised an exception: TypeError", caplog.record_tuples)
def test_get_order_book(default_conf, mocker, order_book_l2):
default_conf['exchange']['name'] = 'binance'
api_mock = MagicMock()

View File

@ -530,10 +530,10 @@ def test_backtest(default_conf, fee, mocker) -> None:
'open_time': [Arrow(2018, 1, 29, 18, 40, 0).datetime,
Arrow(2018, 1, 30, 3, 30, 0).datetime],
'close_time': [Arrow(2018, 1, 29, 22, 35, 0).datetime,
Arrow(2018, 1, 30, 4, 15, 0).datetime],
Arrow(2018, 1, 30, 4, 10, 0).datetime],
'open_index': [78, 184],
'close_index': [125, 193],
'trade_duration': [235, 45],
'close_index': [125, 192],
'trade_duration': [235, 40],
'open_at_end': [False, False],
'open_rate': [0.104445, 0.10302485],
'close_rate': [0.104969, 0.103541],

View File

@ -118,6 +118,7 @@ def test_tickerdata_to_dataframe(default_conf) -> None:
def test_min_roi_reached(default_conf, fee) -> None:
# Use list to confirm sequence does not matter
min_roi_list = [{20: 0.05, 55: 0.01, 0: 0.1},
{0: 0.1, 20: 0.05, 55: 0.01}]
for roi in min_roi_list:
@ -143,6 +144,47 @@ def test_min_roi_reached(default_conf, fee) -> None:
assert strategy.min_roi_reached(trade, 0.02, arrow.utcnow().shift(minutes=-1).datetime)
def test_min_roi_reached2(default_conf, fee) -> None:
# test with ROI raising after last interval
min_roi_list = [{20: 0.07,
30: 0.05,
55: 0.30,
0: 0.1
},
{0: 0.1,
20: 0.07,
30: 0.05,
55: 0.30
},
]
for roi in min_roi_list:
strategy = DefaultStrategy(default_conf)
strategy.minimal_roi = roi
trade = Trade(
pair='ETH/BTC',
stake_amount=0.001,
open_date=arrow.utcnow().shift(hours=-1).datetime,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
open_rate=1,
)
assert not strategy.min_roi_reached(trade, 0.02, arrow.utcnow().shift(minutes=-56).datetime)
assert strategy.min_roi_reached(trade, 0.12, arrow.utcnow().shift(minutes=-56).datetime)
assert not strategy.min_roi_reached(trade, 0.04, arrow.utcnow().shift(minutes=-39).datetime)
assert strategy.min_roi_reached(trade, 0.071, arrow.utcnow().shift(minutes=-39).datetime)
assert not strategy.min_roi_reached(trade, 0.04, arrow.utcnow().shift(minutes=-26).datetime)
assert strategy.min_roi_reached(trade, 0.06, arrow.utcnow().shift(minutes=-26).datetime)
# Should not trigger with 20% profit since after 55 minutes only 30% is active.
assert not strategy.min_roi_reached(trade, 0.20, arrow.utcnow().shift(minutes=-2).datetime)
assert strategy.min_roi_reached(trade, 0.31, arrow.utcnow().shift(minutes=-2).datetime)
def test_analyze_ticker_default(ticker_history, mocker, caplog) -> None:
caplog.set_level(logging.DEBUG)
ind_mock = MagicMock(side_effect=lambda x, meta: x)

View File

@ -1014,6 +1014,211 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
assert trade.is_open is False
def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog,
markets, limit_buy_order, limit_sell_order) -> None:
# When trailing stoploss is set
stoploss_limit = MagicMock(return_value={'id': 13434334})
patch_RPCManager(mocker)
patch_exchange(mocker)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
get_ticker=MagicMock(return_value={
'bid': 0.00001172,
'ask': 0.00001173,
'last': 0.00001172
}),
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
get_fee=fee,
get_markets=markets,
stoploss_limit=stoploss_limit
)
# enabling TSL
default_conf['trailing_stop'] = True
# disabling ROI
default_conf['minimal_roi']['0'] = 999999999
freqtrade = FreqtradeBot(default_conf)
# enabling stoploss on exchange
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
# setting stoploss
freqtrade.strategy.stoploss = -0.05
# setting stoploss_on_exchange_interval to 60 seconds
freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 60
patch_get_signal(freqtrade)
freqtrade.create_trade()
trade = Trade.query.first()
trade.is_open = True
trade.open_order_id = None
trade.stoploss_order_id = 100
stoploss_order_hanging = MagicMock(return_value={
'id': 100,
'status': 'open',
'type': 'stop_loss_limit',
'price': 3,
'average': 2,
'info': {
'stopPrice': '0.000011134'
}
})
mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hanging)
# stoploss initially at 5%
assert freqtrade.handle_trade(trade) is False
assert freqtrade.handle_stoploss_on_exchange(trade) is False
# price jumped 2x
mocker.patch('freqtrade.exchange.Exchange.get_ticker', MagicMock(return_value={
'bid': 0.00002344,
'ask': 0.00002346,
'last': 0.00002344
}))
cancel_order_mock = MagicMock()
stoploss_order_mock = MagicMock()
mocker.patch('freqtrade.exchange.Exchange.cancel_order', cancel_order_mock)
mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_order_mock)
# stoploss should not be updated as the interval is 60 seconds
assert freqtrade.handle_trade(trade) is False
assert freqtrade.handle_stoploss_on_exchange(trade) is False
cancel_order_mock.assert_not_called()
stoploss_order_mock.assert_not_called()
assert freqtrade.handle_trade(trade) is False
assert trade.stop_loss == 0.00002344 * 0.95
# setting stoploss_on_exchange_interval to 0 seconds
freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 0
assert freqtrade.handle_stoploss_on_exchange(trade) is False
cancel_order_mock.assert_called_once_with(100, 'ETH/BTC')
stoploss_order_mock.assert_called_once_with(amount=85.25149190110828,
pair='ETH/BTC',
rate=0.00002344 * 0.95 * 0.99,
stop_price=0.00002344 * 0.95)
def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog,
markets, limit_buy_order, limit_sell_order) -> None:
# When trailing stoploss is set
stoploss_limit = MagicMock(return_value={'id': 13434334})
patch_RPCManager(mocker)
patch_exchange(mocker)
patch_edge(mocker)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
get_ticker=MagicMock(return_value={
'bid': 0.00001172,
'ask': 0.00001173,
'last': 0.00001172
}),
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
get_fee=fee,
get_markets=markets,
stoploss_limit=stoploss_limit
)
# enabling TSL
edge_conf['trailing_stop'] = True
edge_conf['trailing_stop_positive'] = 0.01
edge_conf['trailing_stop_positive_offset'] = 0.011
# disabling ROI
edge_conf['minimal_roi']['0'] = 999999999
freqtrade = FreqtradeBot(edge_conf)
# enabling stoploss on exchange
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
# setting stoploss
freqtrade.strategy.stoploss = -0.02
# setting stoploss_on_exchange_interval to 0 second
freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 0
patch_get_signal(freqtrade)
freqtrade.active_pair_whitelist = freqtrade.edge.adjust(freqtrade.active_pair_whitelist)
freqtrade.create_trade()
trade = Trade.query.first()
trade.is_open = True
trade.open_order_id = None
trade.stoploss_order_id = 100
stoploss_order_hanging = MagicMock(return_value={
'id': 100,
'status': 'open',
'type': 'stop_loss_limit',
'price': 3,
'average': 2,
'info': {
'stopPrice': '0.000009384'
}
})
mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hanging)
# stoploss initially at 20% as edge dictated it.
assert freqtrade.handle_trade(trade) is False
assert freqtrade.handle_stoploss_on_exchange(trade) is False
assert trade.stop_loss == 0.000009384
cancel_order_mock = MagicMock()
stoploss_order_mock = MagicMock()
mocker.patch('freqtrade.exchange.Exchange.cancel_order', cancel_order_mock)
mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_order_mock)
# price goes down 5%
mocker.patch('freqtrade.exchange.Exchange.get_ticker', MagicMock(return_value={
'bid': 0.00001172 * 0.95,
'ask': 0.00001173 * 0.95,
'last': 0.00001172 * 0.95
}))
assert freqtrade.handle_trade(trade) is False
assert freqtrade.handle_stoploss_on_exchange(trade) is False
# stoploss should remain the same
assert trade.stop_loss == 0.000009384
# stoploss on exchange should not be canceled
cancel_order_mock.assert_not_called()
# price jumped 2x
mocker.patch('freqtrade.exchange.Exchange.get_ticker', MagicMock(return_value={
'bid': 0.00002344,
'ask': 0.00002346,
'last': 0.00002344
}))
assert freqtrade.handle_trade(trade) is False
assert freqtrade.handle_stoploss_on_exchange(trade) is False
# stoploss should be set to 1% as trailing is on
assert trade.stop_loss == 0.00002344 * 0.99
cancel_order_mock.assert_called_once_with(100, 'NEO/BTC')
stoploss_order_mock.assert_called_once_with(amount=2131074.168797954,
pair='NEO/BTC',
rate=0.00002344 * 0.99 * 0.99,
stop_price=0.00002344 * 0.99)
def test_process_maybe_execute_buy(mocker, default_conf) -> None:
freqtrade = get_patched_freqtradebot(mocker, default_conf)

View File

@ -516,6 +516,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
assert trade.strategy is None
assert trade.ticker_interval is None
assert trade.stoploss_order_id is None
assert trade.stoploss_last_update is None
assert log_has("trying trades_bak1", caplog.record_tuples)
assert log_has("trying trades_bak2", caplog.record_tuples)
assert log_has("Running database migration - backup available as trades_bak2",

View File

@ -2,7 +2,11 @@
-r requirements.txt
flake8==3.6.0
pytest==4.1.0
flake8-type-annotations==0.1.0
flake8-tidy-imports==1.1.0
pytest==4.1.1
pytest-mock==1.10.0
pytest-asyncio==0.10.0
pytest-cov==2.6.1
coveralls==1.5.1
mypy==0.660

View File

@ -1,17 +1,17 @@
ccxt==1.18.117
SQLAlchemy==1.2.15
ccxt==1.18.144
SQLAlchemy==1.2.16
python-telegram-bot==11.1.0
arrow==0.13.0
cachetools==3.0.0
requests==2.21.0
urllib3==1.24.1
wrapt==1.10.11
wrapt==1.11.1
pandas==0.23.4
scikit-learn==0.20.2
joblib==0.13.0
joblib==0.13.1
scipy==1.2.0
jsonschema==2.6.0
numpy==1.15.4
numpy==1.16.0
TA-Lib==0.4.17
tabulate==0.8.2
coinmarketcap==5.0.3