merged with develop
This commit is contained in:
commit
ee0ebdf0f2
@ -165,6 +165,7 @@ Example to remove the first 10 pairs from the pairlist:
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
"pairlists": [
|
"pairlists": [
|
||||||
|
// ...
|
||||||
{
|
{
|
||||||
"method": "OffsetFilter",
|
"method": "OffsetFilter",
|
||||||
"offset": 10
|
"offset": 10
|
||||||
@ -190,6 +191,19 @@ Sorts pairs by past trade performance, as follows:
|
|||||||
|
|
||||||
Trade count is used as a tie breaker.
|
Trade count is used as a tie breaker.
|
||||||
|
|
||||||
|
You can use the `minutes` parameter to only consider performance of the past X minutes (rolling window).
|
||||||
|
Not defining this parameter (or setting it to 0) will use all-time performance.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"pairlists": [
|
||||||
|
// ...
|
||||||
|
{
|
||||||
|
"method": "PerformanceFilter",
|
||||||
|
"minutes": 1440 // rolling 24h
|
||||||
|
}
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
`PerformanceFilter` does not support backtesting mode.
|
`PerformanceFilter` does not support backtesting mode.
|
||||||
|
|
||||||
|
@ -288,6 +288,12 @@ Stoploss values returned from `custom_stoploss()` always specify a percentage re
|
|||||||
|
|
||||||
The helper function [`stoploss_from_open()`](strategy-customization.md#stoploss_from_open) can be used to convert from an open price relative stop, to a current price relative stop which can be returned from `custom_stoploss()`.
|
The helper function [`stoploss_from_open()`](strategy-customization.md#stoploss_from_open) can be used to convert from an open price relative stop, to a current price relative stop which can be returned from `custom_stoploss()`.
|
||||||
|
|
||||||
|
### Calculating stoploss percentage from absolute price
|
||||||
|
|
||||||
|
Stoploss values returned from `custom_stoploss()` always specify a percentage relative to `current_rate`. In order to set a stoploss at specified absolute price level, we need to use `stop_rate` to calculate what percentage relative to the `current_rate` will give you the same result as if the percentage was specified from the open price.
|
||||||
|
|
||||||
|
The helper function [`stoploss_from_absolute()`](strategy-customization.md#stoploss_from_absolute) can be used to convert from an absolute price, to a current price relative stop which can be returned from `custom_stoploss()`.
|
||||||
|
|
||||||
#### Stepped stoploss
|
#### Stepped stoploss
|
||||||
|
|
||||||
Instead of continuously trailing behind the current price, this example sets fixed stoploss price levels based on the current profit.
|
Instead of continuously trailing behind the current price, this example sets fixed stoploss price levels based on the current profit.
|
||||||
|
@ -639,6 +639,167 @@ Stoploss values returned from `custom_stoploss` must specify a percentage relati
|
|||||||
|
|
||||||
Full examples can be found in the [Custom stoploss](strategy-advanced.md#custom-stoploss) section of the Documentation.
|
Full examples can be found in the [Custom stoploss](strategy-advanced.md#custom-stoploss) section of the Documentation.
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
Providing invalid input to `stoploss_from_open()` may produce "CustomStoploss function did not return valid stoploss" warnings.
|
||||||
|
This may happen if `current_profit` parameter is below specified `open_relative_stop`. Such situations may arise when closing trade
|
||||||
|
is blocked by `confirm_trade_exit()` method. Warnings can be solved by never blocking stop loss sells by checking `sell_reason` in
|
||||||
|
`confirm_trade_exit()`, or by using `return stoploss_from_open(...) or 1` idiom, which will request to not change stop loss when
|
||||||
|
`current_profit < open_relative_stop`.
|
||||||
|
|
||||||
|
### *stoploss_from_absolute()*
|
||||||
|
|
||||||
|
In some situations it may be confusing to deal with stops relative to current rate. Instead, you may define a stoploss level using an absolute price.
|
||||||
|
|
||||||
|
??? Example "Returning a stoploss using absolute price from the custom stoploss function"
|
||||||
|
|
||||||
|
If we want to trail a stop price at 2xATR below current proce we can call `stoploss_from_absolute(current_rate - (candle['atr'] * 2), current_rate)`.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
from freqtrade.strategy import IStrategy, stoploss_from_open
|
||||||
|
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
|
use_custom_stoploss = True
|
||||||
|
|
||||||
|
def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe['atr'] = ta.ATR(dataframe, timeperiod=14)
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||||
|
current_rate: float, current_profit: float, **kwargs) -> float:
|
||||||
|
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||||||
|
candle = dataframe.iloc[-1].squeeze()
|
||||||
|
return stoploss_from_absolute(current_rate - (candle['atr'] * 2), current_rate)
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### *@informative()*
|
||||||
|
|
||||||
|
``` python
|
||||||
|
def informative(timeframe: str, asset: str = '',
|
||||||
|
fmt: Optional[Union[str, Callable[[KwArg(str)], str]]] = None,
|
||||||
|
ffill: bool = True) -> Callable[[PopulateIndicators], PopulateIndicators]:
|
||||||
|
"""
|
||||||
|
A decorator for populate_indicators_Nn(self, dataframe, metadata), allowing these functions to
|
||||||
|
define informative indicators.
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
|
||||||
|
@informative('1h')
|
||||||
|
def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
:param timeframe: Informative timeframe. Must always be equal or higher than strategy timeframe.
|
||||||
|
:param asset: Informative asset, for example BTC, BTC/USDT, ETH/BTC. Do not specify to use
|
||||||
|
current pair.
|
||||||
|
:param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not
|
||||||
|
specified, defaults to:
|
||||||
|
* {base}_{quote}_{column}_{timeframe} if asset is specified.
|
||||||
|
* {column}_{timeframe} if asset is not specified.
|
||||||
|
Format string supports these format variables:
|
||||||
|
* {asset} - full name of the asset, for example 'BTC/USDT'.
|
||||||
|
* {base} - base currency in lower case, for example 'eth'.
|
||||||
|
* {BASE} - same as {base}, except in upper case.
|
||||||
|
* {quote} - quote currency in lower case, for example 'usdt'.
|
||||||
|
* {QUOTE} - same as {quote}, except in upper case.
|
||||||
|
* {column} - name of dataframe column.
|
||||||
|
* {timeframe} - timeframe of informative dataframe.
|
||||||
|
:param ffill: ffill dataframe after merging informative pair.
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
In most common case it is possible to easily define informative pairs by using a decorator. All decorated `populate_indicators_*` methods run in isolation,
|
||||||
|
not having access to data from other informative pairs, in the end all informative dataframes are merged and passed to main `populate_indicators()` method.
|
||||||
|
When hyperopting, use of hyperoptable parameter `.value` attribute is not supported. Please use `.range` attribute. See [optimizing an indicator parameter](hyperopt.md#optimizing-an-indicator-parameter)
|
||||||
|
for more information.
|
||||||
|
|
||||||
|
??? Example "Fast and easy way to define informative pairs"
|
||||||
|
|
||||||
|
Most of the time we do not need power and flexibility offered by `merge_informative_pair()`, therefore we can use a decorator to quickly define informative pairs.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
from freqtrade.strategy import IStrategy, informative
|
||||||
|
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
|
# This method is not required.
|
||||||
|
# def informative_pairs(self): ...
|
||||||
|
|
||||||
|
# Define informative upper timeframe for each pair. Decorators can be stacked on same
|
||||||
|
# method. Available in populate_indicators as 'rsi_30m' and 'rsi_1h'.
|
||||||
|
@informative('30m')
|
||||||
|
@informative('1h')
|
||||||
|
def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
# Define BTC/STAKE informative pair. Available in populate_indicators and other methods as
|
||||||
|
# 'btc_rsi_1h'. Current stake currency should be specified as {stake} format variable
|
||||||
|
# instead of hardcoding actual stake currency. Available in populate_indicators and other
|
||||||
|
# methods as 'btc_usdt_rsi_1h' (when stake currency is USDT).
|
||||||
|
@informative('1h', 'BTC/{stake}')
|
||||||
|
def populate_indicators_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
# Define BTC/ETH informative pair. You must specify quote currency if it is different from
|
||||||
|
# stake currency. Available in populate_indicators and other methods as 'eth_btc_rsi_1h'.
|
||||||
|
@informative('1h', 'ETH/BTC')
|
||||||
|
def populate_indicators_eth_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
# Define BTC/STAKE informative pair. A custom formatter may be specified for formatting
|
||||||
|
# column names. A callable `fmt(**kwargs) -> str` may be specified, to implement custom
|
||||||
|
# formatting. Available in populate_indicators and other methods as 'rsi_upper'.
|
||||||
|
@informative('1h', 'BTC/{stake}', '{column}')
|
||||||
|
def populate_indicators_btc_1h_2(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe['rsi_upper'] = ta.RSI(dataframe, timeperiod=14)
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
# Strategy timeframe indicators for current pair.
|
||||||
|
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
||||||
|
# Informative pairs are available in this method.
|
||||||
|
dataframe['rsi_less'] = dataframe['rsi'] < dataframe['rsi_1h']
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
Do not use `@informative` decorator if you need to use data of one informative pair when generating another informative pair. Instead, define informative pairs
|
||||||
|
manually as described [in the DataProvider section](#complete-data-provider-sample).
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
Use string formatting when accessing informative dataframes of other pairs. This will allow easily changing stake currency in config without having to adjust strategy code.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
stake = self.config['stake_currency']
|
||||||
|
dataframe.loc[
|
||||||
|
(
|
||||||
|
(dataframe[f'btc_{stake}_rsi_1h'] < 35)
|
||||||
|
&
|
||||||
|
(dataframe['volume'] > 0)
|
||||||
|
),
|
||||||
|
['buy', 'buy_tag']] = (1, 'buy_signal_rsi')
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively column renaming may be used to remove stake currency from column names: `@informative('1h', 'BTC/{stake}', fmt='{base}_{column}_{timeframe}')`.
|
||||||
|
|
||||||
|
!!! Warning "Duplicate method names"
|
||||||
|
Methods tagged with `@informative()` decorator must always have unique names! Re-using same name (for example when copy-pasting already defined informative method)
|
||||||
|
will overwrite previously defined method and not produce any errors due to limitations of Python programming language. In such cases you will find that indicators
|
||||||
|
created in earlier-defined methods are not available in the dataframe. Carefully review method names and make sure they are unique!
|
||||||
|
|
||||||
## Additional data (Wallets)
|
## Additional data (Wallets)
|
||||||
|
|
||||||
|
@ -53,7 +53,7 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None:
|
|||||||
|
|
||||||
if epochs and export_csv:
|
if epochs and export_csv:
|
||||||
HyperoptTools.export_csv_file(
|
HyperoptTools.export_csv_file(
|
||||||
config, epochs, total_epochs, not config.get('hyperopt_list_best', False), export_csv
|
config, epochs, export_csv
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -119,7 +119,7 @@ class Edge:
|
|||||||
)
|
)
|
||||||
# Download informative pairs too
|
# Download informative pairs too
|
||||||
res = defaultdict(list)
|
res = defaultdict(list)
|
||||||
for p, t in self.strategy.informative_pairs():
|
for p, t in self.strategy.gather_informative_pairs():
|
||||||
res[t].append(p)
|
res[t].append(p)
|
||||||
for timeframe, inf_pairs in res.items():
|
for timeframe, inf_pairs in res.items():
|
||||||
timerange_startup = deepcopy(self._timerange)
|
timerange_startup = deepcopy(self._timerange)
|
||||||
|
@ -85,10 +85,10 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
self.dataprovider = DataProvider(self.config, self.exchange, self.pairlists)
|
self.dataprovider = DataProvider(self.config, self.exchange, self.pairlists)
|
||||||
|
|
||||||
# Attach Dataprovider to Strategy baseclass
|
# Attach Dataprovider to strategy instance
|
||||||
IStrategy.dp = self.dataprovider
|
self.strategy.dp = self.dataprovider
|
||||||
# Attach Wallets to Strategy baseclass
|
# Attach Wallets to strategy instance
|
||||||
IStrategy.wallets = self.wallets
|
self.strategy.wallets = self.wallets
|
||||||
|
|
||||||
# Initializing Edge only if enabled
|
# Initializing Edge only if enabled
|
||||||
self.edge = Edge(self.config, self.exchange, self.strategy) if \
|
self.edge = Edge(self.config, self.exchange, self.strategy) if \
|
||||||
@ -162,7 +162,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
# Refreshing candles
|
# Refreshing candles
|
||||||
self.dataprovider.refresh(self.pairlists.create_pair_list(self.active_pair_whitelist),
|
self.dataprovider.refresh(self.pairlists.create_pair_list(self.active_pair_whitelist),
|
||||||
self.strategy.informative_pairs())
|
self.strategy.gather_informative_pairs())
|
||||||
|
|
||||||
strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)()
|
strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)()
|
||||||
|
|
||||||
|
@ -154,7 +154,7 @@ class Backtesting:
|
|||||||
self.strategy: IStrategy = strategy
|
self.strategy: IStrategy = strategy
|
||||||
strategy.dp = self.dataprovider
|
strategy.dp = self.dataprovider
|
||||||
# Attach Wallets to Strategy baseclass
|
# Attach Wallets to Strategy baseclass
|
||||||
IStrategy.wallets = self.wallets
|
strategy.wallets = self.wallets
|
||||||
# Set stoploss_on_exchange to false for backtesting,
|
# Set stoploss_on_exchange to false for backtesting,
|
||||||
# since a "perfect" stoploss-sell is assumed anyway
|
# since a "perfect" stoploss-sell is assumed anyway
|
||||||
# And the regular "stoploss" function would not apply to that case
|
# And the regular "stoploss" function would not apply to that case
|
||||||
|
@ -8,6 +8,7 @@ from typing import Any, Dict
|
|||||||
|
|
||||||
from freqtrade import constants
|
from freqtrade import constants
|
||||||
from freqtrade.configuration import TimeRange, validate_config_consistency
|
from freqtrade.configuration import TimeRange, validate_config_consistency
|
||||||
|
from freqtrade.data.dataprovider import DataProvider
|
||||||
from freqtrade.edge import Edge
|
from freqtrade.edge import Edge
|
||||||
from freqtrade.optimize.optimize_reports import generate_edge_table
|
from freqtrade.optimize.optimize_reports import generate_edge_table
|
||||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||||
@ -33,6 +34,7 @@ class EdgeCli:
|
|||||||
self.config['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
|
self.config['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
|
||||||
self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config)
|
self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config)
|
||||||
self.strategy = StrategyResolver.load_strategy(self.config)
|
self.strategy = StrategyResolver.load_strategy(self.config)
|
||||||
|
self.strategy.dp = DataProvider(config, None)
|
||||||
|
|
||||||
validate_config_consistency(self.config)
|
validate_config_consistency(self.config)
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ from pathlib import Path
|
|||||||
from typing import Any, Dict, Iterator, List, Optional, Tuple
|
from typing import Any, Dict, Iterator, List, Optional, Tuple
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
import rapidjson
|
import rapidjson
|
||||||
import tabulate
|
import tabulate
|
||||||
from colorama import Fore, Style
|
from colorama import Fore, Style
|
||||||
@ -298,8 +299,8 @@ class HyperoptTools():
|
|||||||
f"Objective: {results['loss']:.5f}")
|
f"Objective: {results['loss']:.5f}")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def prepare_trials_columns(trials, legacy_mode: bool, has_drawdown: bool) -> str:
|
def prepare_trials_columns(trials: pd.DataFrame, legacy_mode: bool,
|
||||||
|
has_drawdown: bool) -> pd.DataFrame:
|
||||||
trials['Best'] = ''
|
trials['Best'] = ''
|
||||||
|
|
||||||
if 'results_metrics.winsdrawslosses' not in trials.columns:
|
if 'results_metrics.winsdrawslosses' not in trials.columns:
|
||||||
@ -435,8 +436,7 @@ class HyperoptTools():
|
|||||||
return table
|
return table
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def export_csv_file(config: dict, results: list, total_epochs: int, highlight_best: bool,
|
def export_csv_file(config: dict, results: list, csv_file: str) -> None:
|
||||||
csv_file: str) -> None:
|
|
||||||
"""
|
"""
|
||||||
Log result to csv-file
|
Log result to csv-file
|
||||||
"""
|
"""
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
This module contains the class to persist trades into SQLite
|
This module contains the class to persist trades into SQLite
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
@ -1026,17 +1026,21 @@ class Trade(_DECL_BASE, LocalTrade):
|
|||||||
return total_open_stake_amount or 0
|
return total_open_stake_amount or 0
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_overall_performance() -> List[Dict[str, Any]]:
|
def get_overall_performance(minutes=None) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Returns List of dicts containing all Trades, including profit and trade count
|
Returns List of dicts containing all Trades, including profit and trade count
|
||||||
NOTE: Not supported in Backtesting.
|
NOTE: Not supported in Backtesting.
|
||||||
"""
|
"""
|
||||||
|
filters = [Trade.is_open.is_(False)]
|
||||||
|
if minutes:
|
||||||
|
start_date = datetime.now(timezone.utc) - timedelta(minutes=minutes)
|
||||||
|
filters.append(Trade.close_date >= start_date)
|
||||||
pair_rates = Trade.query.with_entities(
|
pair_rates = Trade.query.with_entities(
|
||||||
Trade.pair,
|
Trade.pair,
|
||||||
func.sum(Trade.close_profit).label('profit_sum'),
|
func.sum(Trade.close_profit).label('profit_sum'),
|
||||||
func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
|
func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
|
||||||
func.count(Trade.pair).label('count')
|
func.count(Trade.pair).label('count')
|
||||||
).filter(Trade.is_open.is_(False))\
|
).filter(*filters)\
|
||||||
.group_by(Trade.pair) \
|
.group_by(Trade.pair) \
|
||||||
.order_by(desc('profit_sum_abs')) \
|
.order_by(desc('profit_sum_abs')) \
|
||||||
.all()
|
.all()
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
Performance pair list filter
|
Performance pair list filter
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
@ -15,6 +15,13 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
class PerformanceFilter(IPairList):
|
class PerformanceFilter(IPairList):
|
||||||
|
|
||||||
|
def __init__(self, exchange, pairlistmanager,
|
||||||
|
config: Dict[str, Any], pairlistconfig: Dict[str, Any],
|
||||||
|
pairlist_pos: int) -> None:
|
||||||
|
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||||
|
|
||||||
|
self._minutes = pairlistconfig.get('minutes', 0)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def needstickers(self) -> bool:
|
def needstickers(self) -> bool:
|
||||||
"""
|
"""
|
||||||
@ -40,7 +47,7 @@ class PerformanceFilter(IPairList):
|
|||||||
"""
|
"""
|
||||||
# Get the trading performance for pairs from database
|
# Get the trading performance for pairs from database
|
||||||
try:
|
try:
|
||||||
performance = pd.DataFrame(Trade.get_overall_performance())
|
performance = pd.DataFrame(Trade.get_overall_performance(self._minutes))
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
# Performancefilter does not work in backtesting.
|
# Performancefilter does not work in backtesting.
|
||||||
self.log_once("PerformanceFilter is not available in this mode.", logger.warning)
|
self.log_once("PerformanceFilter is not available in this mode.", logger.warning)
|
||||||
|
@ -46,6 +46,12 @@ class Balances(BaseModel):
|
|||||||
value: float
|
value: float
|
||||||
stake: str
|
stake: str
|
||||||
note: str
|
note: str
|
||||||
|
starting_capital: float
|
||||||
|
starting_capital_ratio: float
|
||||||
|
starting_capital_pct: float
|
||||||
|
starting_capital_fiat: float
|
||||||
|
starting_capital_fiat_ratio: float
|
||||||
|
starting_capital_fiat_pct: float
|
||||||
|
|
||||||
|
|
||||||
class Count(BaseModel):
|
class Count(BaseModel):
|
||||||
|
@ -459,6 +459,9 @@ class RPC:
|
|||||||
raise RPCException('Error getting current tickers.')
|
raise RPCException('Error getting current tickers.')
|
||||||
|
|
||||||
self._freqtrade.wallets.update(require_update=False)
|
self._freqtrade.wallets.update(require_update=False)
|
||||||
|
starting_capital = self._freqtrade.wallets.get_starting_balance()
|
||||||
|
starting_cap_fiat = self._fiat_converter.convert_amount(
|
||||||
|
starting_capital, stake_currency, fiat_display_currency) if self._fiat_converter else 0
|
||||||
|
|
||||||
for coin, balance in self._freqtrade.wallets.get_all_balances().items():
|
for coin, balance in self._freqtrade.wallets.get_all_balances().items():
|
||||||
if not balance.total:
|
if not balance.total:
|
||||||
@ -494,15 +497,25 @@ class RPC:
|
|||||||
else:
|
else:
|
||||||
raise RPCException('All balances are zero.')
|
raise RPCException('All balances are zero.')
|
||||||
|
|
||||||
symbol = fiat_display_currency
|
value = self._fiat_converter.convert_amount(
|
||||||
value = self._fiat_converter.convert_amount(total, stake_currency,
|
total, stake_currency, fiat_display_currency) if self._fiat_converter else 0
|
||||||
symbol) if self._fiat_converter else 0
|
|
||||||
|
starting_capital_ratio = 0.0
|
||||||
|
starting_capital_ratio = (total / starting_capital) - 1 if starting_capital else 0.0
|
||||||
|
starting_cap_fiat_ratio = (value / starting_cap_fiat) - 1 if starting_cap_fiat else 0.0
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'currencies': output,
|
'currencies': output,
|
||||||
'total': total,
|
'total': total,
|
||||||
'symbol': symbol,
|
'symbol': fiat_display_currency,
|
||||||
'value': value,
|
'value': value,
|
||||||
'stake': stake_currency,
|
'stake': stake_currency,
|
||||||
|
'starting_capital': starting_capital,
|
||||||
|
'starting_capital_ratio': starting_capital_ratio,
|
||||||
|
'starting_capital_pct': round(starting_capital_ratio * 100, 2),
|
||||||
|
'starting_capital_fiat': starting_cap_fiat,
|
||||||
|
'starting_capital_fiat_ratio': starting_cap_fiat_ratio,
|
||||||
|
'starting_capital_fiat_pct': round(starting_cap_fiat_ratio * 100, 2),
|
||||||
'note': 'Simulated balances' if self._freqtrade.config['dry_run'] else ''
|
'note': 'Simulated balances' if self._freqtrade.config['dry_run'] else ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -603,12 +603,15 @@ class Telegram(RPCHandler):
|
|||||||
|
|
||||||
output = ''
|
output = ''
|
||||||
if self._config['dry_run']:
|
if self._config['dry_run']:
|
||||||
output += (
|
output += "*Warning:* Simulated balances in Dry Mode.\n"
|
||||||
f"*Warning:* Simulated balances in Dry Mode.\n"
|
|
||||||
"This mode is still experimental!\n"
|
output += ("Starting capital: "
|
||||||
"Starting capital: "
|
f"`{result['starting_capital']}` {self._config['stake_currency']}"
|
||||||
f"`{self._config['dry_run_wallet']}` {self._config['stake_currency']}.\n"
|
)
|
||||||
)
|
output += (f" `{result['starting_capital_fiat']}` "
|
||||||
|
f"{self._config['fiat_display_currency']}.\n"
|
||||||
|
) if result['starting_capital_fiat'] > 0 else '.\n'
|
||||||
|
|
||||||
total_dust_balance = 0
|
total_dust_balance = 0
|
||||||
total_dust_currencies = 0
|
total_dust_currencies = 0
|
||||||
for curr in result['currencies']:
|
for curr in result['currencies']:
|
||||||
@ -641,9 +644,12 @@ class Telegram(RPCHandler):
|
|||||||
f"{round_coin_value(total_dust_balance, result['stake'], False)}`\n")
|
f"{round_coin_value(total_dust_balance, result['stake'], False)}`\n")
|
||||||
|
|
||||||
output += ("\n*Estimated Value*:\n"
|
output += ("\n*Estimated Value*:\n"
|
||||||
f"\t`{result['stake']}: {result['total']: .8f}`\n"
|
f"\t`{result['stake']}: "
|
||||||
|
f"{round_coin_value(result['total'], result['stake'], False)}`"
|
||||||
|
f" `({result['starting_capital_pct']}%)`\n"
|
||||||
f"\t`{result['symbol']}: "
|
f"\t`{result['symbol']}: "
|
||||||
f"{round_coin_value(result['value'], result['symbol'], False)}`\n")
|
f"{round_coin_value(result['value'], result['symbol'], False)}`"
|
||||||
|
f" `({result['starting_capital_fiat_pct']}%)`\n")
|
||||||
self._send_msg(output, reload_able=True, callback_path="update_balance",
|
self._send_msg(output, reload_able=True, callback_path="update_balance",
|
||||||
query=update.callback_query)
|
query=update.callback_query)
|
||||||
except RPCException as e:
|
except RPCException as e:
|
||||||
|
@ -3,5 +3,7 @@ from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timefr
|
|||||||
timeframe_to_prev_date, timeframe_to_seconds)
|
timeframe_to_prev_date, timeframe_to_seconds)
|
||||||
from freqtrade.strategy.hyper import (BooleanParameter, CategoricalParameter, DecimalParameter,
|
from freqtrade.strategy.hyper import (BooleanParameter, CategoricalParameter, DecimalParameter,
|
||||||
IntParameter, RealParameter)
|
IntParameter, RealParameter)
|
||||||
|
from freqtrade.strategy.informative_decorator import informative
|
||||||
from freqtrade.strategy.interface import IStrategy
|
from freqtrade.strategy.interface import IStrategy
|
||||||
from freqtrade.strategy.strategy_helper import merge_informative_pair, stoploss_from_open
|
from freqtrade.strategy.strategy_helper import (merge_informative_pair, stoploss_from_absolute,
|
||||||
|
stoploss_from_open)
|
||||||
|
128
freqtrade/strategy/informative_decorator.py
Normal file
128
freqtrade/strategy/informative_decorator.py
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
from typing import Any, Callable, NamedTuple, Optional, Union
|
||||||
|
|
||||||
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from freqtrade.exceptions import OperationalException
|
||||||
|
from freqtrade.strategy.strategy_helper import merge_informative_pair
|
||||||
|
|
||||||
|
|
||||||
|
PopulateIndicators = Callable[[Any, DataFrame, dict], DataFrame]
|
||||||
|
|
||||||
|
|
||||||
|
class InformativeData(NamedTuple):
|
||||||
|
asset: Optional[str]
|
||||||
|
timeframe: str
|
||||||
|
fmt: Union[str, Callable[[Any], str], None]
|
||||||
|
ffill: bool
|
||||||
|
|
||||||
|
|
||||||
|
def informative(timeframe: str, asset: str = '',
|
||||||
|
fmt: Optional[Union[str, Callable[[Any], str]]] = None,
|
||||||
|
ffill: bool = True) -> Callable[[PopulateIndicators], PopulateIndicators]:
|
||||||
|
"""
|
||||||
|
A decorator for populate_indicators_Nn(self, dataframe, metadata), allowing these functions to
|
||||||
|
define informative indicators.
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
|
||||||
|
@informative('1h')
|
||||||
|
def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
:param timeframe: Informative timeframe. Must always be equal or higher than strategy timeframe.
|
||||||
|
:param asset: Informative asset, for example BTC, BTC/USDT, ETH/BTC. Do not specify to use
|
||||||
|
current pair.
|
||||||
|
:param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not
|
||||||
|
specified, defaults to:
|
||||||
|
* {base}_{quote}_{column}_{timeframe} if asset is specified.
|
||||||
|
* {column}_{timeframe} if asset is not specified.
|
||||||
|
Format string supports these format variables:
|
||||||
|
* {asset} - full name of the asset, for example 'BTC/USDT'.
|
||||||
|
* {base} - base currency in lower case, for example 'eth'.
|
||||||
|
* {BASE} - same as {base}, except in upper case.
|
||||||
|
* {quote} - quote currency in lower case, for example 'usdt'.
|
||||||
|
* {QUOTE} - same as {quote}, except in upper case.
|
||||||
|
* {column} - name of dataframe column.
|
||||||
|
* {timeframe} - timeframe of informative dataframe.
|
||||||
|
:param ffill: ffill dataframe after merging informative pair.
|
||||||
|
"""
|
||||||
|
_asset = asset
|
||||||
|
_timeframe = timeframe
|
||||||
|
_fmt = fmt
|
||||||
|
_ffill = ffill
|
||||||
|
|
||||||
|
def decorator(fn: PopulateIndicators):
|
||||||
|
informative_pairs = getattr(fn, '_ft_informative', [])
|
||||||
|
informative_pairs.append(InformativeData(_asset, _timeframe, _fmt, _ffill))
|
||||||
|
setattr(fn, '_ft_informative', informative_pairs)
|
||||||
|
return fn
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def _format_pair_name(config, pair: str) -> str:
|
||||||
|
return pair.format(stake_currency=config['stake_currency'],
|
||||||
|
stake=config['stake_currency']).upper()
|
||||||
|
|
||||||
|
|
||||||
|
def _create_and_merge_informative_pair(strategy, dataframe: DataFrame, metadata: dict,
|
||||||
|
inf_data: InformativeData,
|
||||||
|
populate_indicators: PopulateIndicators):
|
||||||
|
asset = inf_data.asset or ''
|
||||||
|
timeframe = inf_data.timeframe
|
||||||
|
fmt = inf_data.fmt
|
||||||
|
config = strategy.config
|
||||||
|
|
||||||
|
if asset:
|
||||||
|
# Insert stake currency if needed.
|
||||||
|
asset = _format_pair_name(config, asset)
|
||||||
|
else:
|
||||||
|
# Not specifying an asset will define informative dataframe for current pair.
|
||||||
|
asset = metadata['pair']
|
||||||
|
|
||||||
|
if '/' in asset:
|
||||||
|
base, quote = asset.split('/')
|
||||||
|
else:
|
||||||
|
# When futures are supported this may need reevaluation.
|
||||||
|
# base, quote = asset, ''
|
||||||
|
raise OperationalException('Not implemented.')
|
||||||
|
|
||||||
|
# Default format. This optimizes for the common case: informative pairs using same stake
|
||||||
|
# currency. When quote currency matches stake currency, column name will omit base currency.
|
||||||
|
# This allows easily reconfiguring strategy to use different base currency. In a rare case
|
||||||
|
# where it is desired to keep quote currency in column name at all times user should specify
|
||||||
|
# fmt='{base}_{quote}_{column}_{timeframe}' format or similar.
|
||||||
|
if not fmt:
|
||||||
|
fmt = '{column}_{timeframe}' # Informatives of current pair
|
||||||
|
if inf_data.asset:
|
||||||
|
fmt = '{base}_{quote}_' + fmt # Informatives of other pairs
|
||||||
|
|
||||||
|
inf_metadata = {'pair': asset, 'timeframe': timeframe}
|
||||||
|
inf_dataframe = strategy.dp.get_pair_dataframe(asset, timeframe)
|
||||||
|
inf_dataframe = populate_indicators(strategy, inf_dataframe, inf_metadata)
|
||||||
|
|
||||||
|
formatter: Any = None
|
||||||
|
if callable(fmt):
|
||||||
|
formatter = fmt # A custom user-specified formatter function.
|
||||||
|
else:
|
||||||
|
formatter = fmt.format # A default string formatter.
|
||||||
|
|
||||||
|
fmt_args = {
|
||||||
|
'BASE': base.upper(),
|
||||||
|
'QUOTE': quote.upper(),
|
||||||
|
'base': base.lower(),
|
||||||
|
'quote': quote.lower(),
|
||||||
|
'asset': asset,
|
||||||
|
'timeframe': timeframe,
|
||||||
|
}
|
||||||
|
inf_dataframe.rename(columns=lambda column: formatter(column=column, **fmt_args),
|
||||||
|
inplace=True)
|
||||||
|
|
||||||
|
date_column = formatter(column='date', **fmt_args)
|
||||||
|
if date_column in dataframe.columns:
|
||||||
|
raise OperationalException(f'Duplicate column name {date_column} exists in '
|
||||||
|
f'dataframe! Ensure column names are unique!')
|
||||||
|
dataframe = merge_informative_pair(dataframe, inf_dataframe, strategy.timeframe, timeframe,
|
||||||
|
ffill=inf_data.ffill, append_timeframe=False,
|
||||||
|
date_column=date_column)
|
||||||
|
return dataframe
|
@ -19,6 +19,9 @@ from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
|
|||||||
from freqtrade.exchange.exchange import timeframe_to_next_date
|
from freqtrade.exchange.exchange import timeframe_to_next_date
|
||||||
from freqtrade.persistence import PairLocks, Trade
|
from freqtrade.persistence import PairLocks, Trade
|
||||||
from freqtrade.strategy.hyper import HyperStrategyMixin
|
from freqtrade.strategy.hyper import HyperStrategyMixin
|
||||||
|
from freqtrade.strategy.informative_decorator import (InformativeData, PopulateIndicators,
|
||||||
|
_create_and_merge_informative_pair,
|
||||||
|
_format_pair_name)
|
||||||
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
||||||
from freqtrade.wallets import Wallets
|
from freqtrade.wallets import Wallets
|
||||||
|
|
||||||
@ -118,7 +121,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
# Class level variables (intentional) containing
|
# Class level variables (intentional) containing
|
||||||
# the dataprovider (dp) (access to other candles, historic data, ...)
|
# the dataprovider (dp) (access to other candles, historic data, ...)
|
||||||
# and wallets - access to the current balance.
|
# and wallets - access to the current balance.
|
||||||
dp: Optional[DataProvider] = None
|
dp: Optional[DataProvider]
|
||||||
wallets: Optional[Wallets] = None
|
wallets: Optional[Wallets] = None
|
||||||
# Filled from configuration
|
# Filled from configuration
|
||||||
stake_currency: str
|
stake_currency: str
|
||||||
@ -134,6 +137,24 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
self._last_candle_seen_per_pair: Dict[str, datetime] = {}
|
self._last_candle_seen_per_pair: Dict[str, datetime] = {}
|
||||||
super().__init__(config)
|
super().__init__(config)
|
||||||
|
|
||||||
|
# Gather informative pairs from @informative-decorated methods.
|
||||||
|
self._ft_informative: List[Tuple[InformativeData, PopulateIndicators]] = []
|
||||||
|
for attr_name in dir(self.__class__):
|
||||||
|
cls_method = getattr(self.__class__, attr_name)
|
||||||
|
if not callable(cls_method):
|
||||||
|
continue
|
||||||
|
informative_data_list = getattr(cls_method, '_ft_informative', None)
|
||||||
|
if not isinstance(informative_data_list, list):
|
||||||
|
# Type check is required because mocker would return a mock object that evaluates to
|
||||||
|
# True, confusing this code.
|
||||||
|
continue
|
||||||
|
strategy_timeframe_minutes = timeframe_to_minutes(self.timeframe)
|
||||||
|
for informative_data in informative_data_list:
|
||||||
|
if timeframe_to_minutes(informative_data.timeframe) < strategy_timeframe_minutes:
|
||||||
|
raise OperationalException('Informative timeframe must be equal or higher than '
|
||||||
|
'strategy timeframe!')
|
||||||
|
self._ft_informative.append((informative_data, cls_method))
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
@ -377,6 +398,23 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
# END - Intended to be overridden by strategy
|
# END - Intended to be overridden by strategy
|
||||||
###
|
###
|
||||||
|
|
||||||
|
def gather_informative_pairs(self) -> ListPairsWithTimeframes:
|
||||||
|
"""
|
||||||
|
Internal method which gathers all informative pairs (user or automatically defined).
|
||||||
|
"""
|
||||||
|
informative_pairs = self.informative_pairs()
|
||||||
|
for inf_data, _ in self._ft_informative:
|
||||||
|
if inf_data.asset:
|
||||||
|
pair_tf = (_format_pair_name(self.config, inf_data.asset), inf_data.timeframe)
|
||||||
|
informative_pairs.append(pair_tf)
|
||||||
|
else:
|
||||||
|
if not self.dp:
|
||||||
|
raise OperationalException('@informative decorator with unspecified asset '
|
||||||
|
'requires DataProvider instance.')
|
||||||
|
for pair in self.dp.current_whitelist():
|
||||||
|
informative_pairs.append((pair, inf_data.timeframe))
|
||||||
|
return list(set(informative_pairs))
|
||||||
|
|
||||||
def get_strategy_name(self) -> str:
|
def get_strategy_name(self) -> str:
|
||||||
"""
|
"""
|
||||||
Returns strategy class name
|
Returns strategy class name
|
||||||
@ -802,6 +840,12 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
:return: a Dataframe with all mandatory indicators for the strategies
|
:return: a Dataframe with all mandatory indicators for the strategies
|
||||||
"""
|
"""
|
||||||
logger.debug(f"Populating indicators for pair {metadata.get('pair')}.")
|
logger.debug(f"Populating indicators for pair {metadata.get('pair')}.")
|
||||||
|
|
||||||
|
# call populate_indicators_Nm() which were tagged with @informative decorator.
|
||||||
|
for inf_data, populate_fn in self._ft_informative:
|
||||||
|
dataframe = _create_and_merge_informative_pair(
|
||||||
|
self, dataframe, metadata, inf_data, populate_fn)
|
||||||
|
|
||||||
if self._populate_fun_len == 2:
|
if self._populate_fun_len == 2:
|
||||||
warnings.warn("deprecated - check out the Sample strategy to see "
|
warnings.warn("deprecated - check out the Sample strategy to see "
|
||||||
"the current function headers!", DeprecationWarning)
|
"the current function headers!", DeprecationWarning)
|
||||||
|
@ -4,7 +4,9 @@ from freqtrade.exchange import timeframe_to_minutes
|
|||||||
|
|
||||||
|
|
||||||
def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame,
|
def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame,
|
||||||
timeframe: str, timeframe_inf: str, ffill: bool = True) -> pd.DataFrame:
|
timeframe: str, timeframe_inf: str, ffill: bool = True,
|
||||||
|
append_timeframe: bool = True,
|
||||||
|
date_column: str = 'date') -> pd.DataFrame:
|
||||||
"""
|
"""
|
||||||
Correctly merge informative samples to the original dataframe, avoiding lookahead bias.
|
Correctly merge informative samples to the original dataframe, avoiding lookahead bias.
|
||||||
|
|
||||||
@ -24,6 +26,8 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame,
|
|||||||
:param timeframe: Timeframe of the original pair sample.
|
:param timeframe: Timeframe of the original pair sample.
|
||||||
:param timeframe_inf: Timeframe of the informative pair sample.
|
:param timeframe_inf: Timeframe of the informative pair sample.
|
||||||
:param ffill: Forwardfill missing values - optional but usually required
|
:param ffill: Forwardfill missing values - optional but usually required
|
||||||
|
:param append_timeframe: Rename columns by appending timeframe.
|
||||||
|
:param date_column: A custom date column name.
|
||||||
:return: Merged dataframe
|
:return: Merged dataframe
|
||||||
:raise: ValueError if the secondary timeframe is shorter than the dataframe timeframe
|
:raise: ValueError if the secondary timeframe is shorter than the dataframe timeframe
|
||||||
"""
|
"""
|
||||||
@ -32,25 +36,29 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame,
|
|||||||
minutes = timeframe_to_minutes(timeframe)
|
minutes = timeframe_to_minutes(timeframe)
|
||||||
if minutes == minutes_inf:
|
if minutes == minutes_inf:
|
||||||
# No need to forwardshift if the timeframes are identical
|
# No need to forwardshift if the timeframes are identical
|
||||||
informative['date_merge'] = informative["date"]
|
informative['date_merge'] = informative[date_column]
|
||||||
elif minutes < minutes_inf:
|
elif minutes < minutes_inf:
|
||||||
# Subtract "small" timeframe so merging is not delayed by 1 small candle
|
# Subtract "small" timeframe so merging is not delayed by 1 small candle
|
||||||
# Detailed explanation in https://github.com/freqtrade/freqtrade/issues/4073
|
# Detailed explanation in https://github.com/freqtrade/freqtrade/issues/4073
|
||||||
informative['date_merge'] = (
|
informative['date_merge'] = (
|
||||||
informative["date"] + pd.to_timedelta(minutes_inf, 'm') - pd.to_timedelta(minutes, 'm')
|
informative[date_column] + pd.to_timedelta(minutes_inf, 'm') -
|
||||||
|
pd.to_timedelta(minutes, 'm')
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise ValueError("Tried to merge a faster timeframe to a slower timeframe."
|
raise ValueError("Tried to merge a faster timeframe to a slower timeframe."
|
||||||
"This would create new rows, and can throw off your regular indicators.")
|
"This would create new rows, and can throw off your regular indicators.")
|
||||||
|
|
||||||
# Rename columns to be unique
|
# Rename columns to be unique
|
||||||
informative.columns = [f"{col}_{timeframe_inf}" for col in informative.columns]
|
date_merge = 'date_merge'
|
||||||
|
if append_timeframe:
|
||||||
|
date_merge = f'date_merge_{timeframe_inf}'
|
||||||
|
informative.columns = [f"{col}_{timeframe_inf}" for col in informative.columns]
|
||||||
|
|
||||||
# Combine the 2 dataframes
|
# Combine the 2 dataframes
|
||||||
# all indicators on the informative sample MUST be calculated before this point
|
# all indicators on the informative sample MUST be calculated before this point
|
||||||
dataframe = pd.merge(dataframe, informative, left_on='date',
|
dataframe = pd.merge(dataframe, informative, left_on='date',
|
||||||
right_on=f'date_merge_{timeframe_inf}', how='left')
|
right_on=date_merge, how='left')
|
||||||
dataframe = dataframe.drop(f'date_merge_{timeframe_inf}', axis=1)
|
dataframe = dataframe.drop(date_merge, axis=1)
|
||||||
|
|
||||||
if ffill:
|
if ffill:
|
||||||
dataframe = dataframe.ffill()
|
dataframe = dataframe.ffill()
|
||||||
@ -83,3 +91,28 @@ def stoploss_from_open(open_relative_stop: float, current_profit: float) -> floa
|
|||||||
|
|
||||||
# negative stoploss values indicate the requested stop price is higher than the current price
|
# negative stoploss values indicate the requested stop price is higher than the current price
|
||||||
return max(stoploss, 0.0)
|
return max(stoploss, 0.0)
|
||||||
|
|
||||||
|
|
||||||
|
def stoploss_from_absolute(stop_rate: float, current_rate: float) -> float:
|
||||||
|
"""
|
||||||
|
Given current price and desired stop price, return a stop loss value that is relative to current
|
||||||
|
price.
|
||||||
|
|
||||||
|
The requested stop can be positive for a stop above the open price, or negative for
|
||||||
|
a stop below the open price. The return value is always >= 0.
|
||||||
|
|
||||||
|
Returns 0 if the resulting stop price would be above the current price.
|
||||||
|
|
||||||
|
:param stop_rate: Stop loss price.
|
||||||
|
:param current_rate: Current asset price.
|
||||||
|
:return: Positive stop loss value relative to current price
|
||||||
|
"""
|
||||||
|
|
||||||
|
# formula is undefined for current_rate 0, return maximum value
|
||||||
|
if current_rate == 0:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
stoploss = 1 - (stop_rate / current_rate)
|
||||||
|
|
||||||
|
# negative stoploss values indicate the requested stop price is higher than the current price
|
||||||
|
return max(stoploss, 0.0)
|
||||||
|
2
setup.sh
2
setup.sh
@ -62,7 +62,7 @@ function updateenv() {
|
|||||||
then
|
then
|
||||||
REQUIREMENTS_PLOT="-r requirements-plot.txt"
|
REQUIREMENTS_PLOT="-r requirements-plot.txt"
|
||||||
fi
|
fi
|
||||||
if [ "${SYS_ARCH}" == "armv7l" ]; then
|
if [ "${SYS_ARCH}" == "armv7l" ] || [ "${SYS_ARCH}" == "armv6l" ]; then
|
||||||
echo "Detected Raspberry, installing cython, skipping hyperopt installation."
|
echo "Detected Raspberry, installing cython, skipping hyperopt installation."
|
||||||
${PYTHON} -m pip install --upgrade cython
|
${PYTHON} -m pip install --upgrade cython
|
||||||
else
|
else
|
||||||
|
@ -12,7 +12,8 @@ from freqtrade.persistence import Trade
|
|||||||
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
||||||
from freqtrade.plugins.pairlistmanager import PairListManager
|
from freqtrade.plugins.pairlistmanager import PairListManager
|
||||||
from freqtrade.resolvers import PairListResolver
|
from freqtrade.resolvers import PairListResolver
|
||||||
from tests.conftest import get_patched_exchange, get_patched_freqtradebot, log_has, log_has_re
|
from tests.conftest import (create_mock_trades, get_patched_exchange, get_patched_freqtradebot,
|
||||||
|
log_has, log_has_re)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
@ -663,6 +664,31 @@ def test_PerformanceFilter_error(mocker, whitelist_conf, caplog) -> None:
|
|||||||
assert log_has("PerformanceFilter is not available in this mode.", caplog)
|
assert log_has("PerformanceFilter is not available in this mode.", caplog)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
|
def test_PerformanceFilter_lookback(mocker, whitelist_conf, fee) -> None:
|
||||||
|
whitelist_conf['exchange']['pair_whitelist'].append('XRP/BTC')
|
||||||
|
whitelist_conf['pairlists'] = [
|
||||||
|
{"method": "StaticPairList"},
|
||||||
|
{"method": "PerformanceFilter", "minutes": 60}
|
||||||
|
]
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
||||||
|
exchange = get_patched_exchange(mocker, whitelist_conf)
|
||||||
|
pm = PairListManager(exchange, whitelist_conf)
|
||||||
|
pm.refresh_pairlist()
|
||||||
|
|
||||||
|
assert pm.whitelist == ['ETH/BTC', 'TKN/BTC', 'XRP/BTC']
|
||||||
|
|
||||||
|
with time_machine.travel("2021-09-01 05:00:00 +00:00") as t:
|
||||||
|
create_mock_trades(fee)
|
||||||
|
pm.refresh_pairlist()
|
||||||
|
assert pm.whitelist == ['XRP/BTC', 'ETH/BTC', 'TKN/BTC']
|
||||||
|
|
||||||
|
# Move to "outside" of lookback window, so original sorting is restored.
|
||||||
|
t.move_to("2021-09-01 07:00:00 +00:00")
|
||||||
|
pm.refresh_pairlist()
|
||||||
|
assert pm.whitelist == ['ETH/BTC', 'TKN/BTC', 'XRP/BTC']
|
||||||
|
|
||||||
|
|
||||||
def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None:
|
def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None:
|
||||||
default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}]
|
default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}]
|
||||||
|
|
||||||
|
@ -422,20 +422,22 @@ def test_api_stopbuy(botclient):
|
|||||||
assert ftbot.config['max_open_trades'] == 0
|
assert ftbot.config['max_open_trades'] == 0
|
||||||
|
|
||||||
|
|
||||||
def test_api_balance(botclient, mocker, rpc_balance):
|
def test_api_balance(botclient, mocker, rpc_balance, tickers):
|
||||||
ftbot, client = botclient
|
ftbot, client = botclient
|
||||||
|
|
||||||
ftbot.config['dry_run'] = False
|
ftbot.config['dry_run'] = False
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance)
|
mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_valid_pair_combination',
|
mocker.patch('freqtrade.exchange.Exchange.get_valid_pair_combination',
|
||||||
side_effect=lambda a, b: f"{a}/{b}")
|
side_effect=lambda a, b: f"{a}/{b}")
|
||||||
ftbot.wallets.update()
|
ftbot.wallets.update()
|
||||||
|
|
||||||
rc = client_get(client, f"{BASE_URI}/balance")
|
rc = client_get(client, f"{BASE_URI}/balance")
|
||||||
assert_response(rc)
|
assert_response(rc)
|
||||||
assert "currencies" in rc.json()
|
response = rc.json()
|
||||||
assert len(rc.json()["currencies"]) == 5
|
assert "currencies" in response
|
||||||
assert rc.json()['currencies'][0] == {
|
assert len(response["currencies"]) == 5
|
||||||
|
assert response['currencies'][0] == {
|
||||||
'currency': 'BTC',
|
'currency': 'BTC',
|
||||||
'free': 12.0,
|
'free': 12.0,
|
||||||
'balance': 12.0,
|
'balance': 12.0,
|
||||||
@ -443,6 +445,10 @@ def test_api_balance(botclient, mocker, rpc_balance):
|
|||||||
'est_stake': 12.0,
|
'est_stake': 12.0,
|
||||||
'stake': 'BTC',
|
'stake': 'BTC',
|
||||||
}
|
}
|
||||||
|
assert 'starting_capital' in response
|
||||||
|
assert 'starting_capital_fiat' in response
|
||||||
|
assert 'starting_capital_pct' in response
|
||||||
|
assert 'starting_capital_ratio' in response
|
||||||
|
|
||||||
|
|
||||||
def test_api_count(botclient, mocker, ticker, fee, markets):
|
def test_api_count(botclient, mocker, ticker, fee, markets):
|
||||||
@ -1218,6 +1224,7 @@ def test_api_strategies(botclient):
|
|||||||
assert_response(rc)
|
assert_response(rc)
|
||||||
assert rc.json() == {'strategies': [
|
assert rc.json() == {'strategies': [
|
||||||
'HyperoptableStrategy',
|
'HyperoptableStrategy',
|
||||||
|
'InformativeDecoratorTest',
|
||||||
'StrategyTestV2',
|
'StrategyTestV2',
|
||||||
'TestStrategyLegacyV1'
|
'TestStrategyLegacyV1'
|
||||||
]}
|
]}
|
||||||
|
@ -576,6 +576,8 @@ def test_balance_handle_too_large_response(default_conf, update, mocker) -> None
|
|||||||
'total': 100.0,
|
'total': 100.0,
|
||||||
'symbol': 100.0,
|
'symbol': 100.0,
|
||||||
'value': 1000.0,
|
'value': 1000.0,
|
||||||
|
'starting_capital': 1000,
|
||||||
|
'starting_capital_fiat': 1000,
|
||||||
})
|
})
|
||||||
|
|
||||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||||
|
75
tests/strategy/strats/informative_decorator_strategy.py
Normal file
75
tests/strategy/strats/informative_decorator_strategy.py
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
|
||||||
|
|
||||||
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from freqtrade.strategy import informative, merge_informative_pair
|
||||||
|
from freqtrade.strategy.interface import IStrategy
|
||||||
|
|
||||||
|
|
||||||
|
class InformativeDecoratorTest(IStrategy):
|
||||||
|
"""
|
||||||
|
Strategy used by tests freqtrade bot.
|
||||||
|
Please do not modify this strategy, it's intended for internal use only.
|
||||||
|
Please look at the SampleStrategy in the user_data/strategy directory
|
||||||
|
or strategy repository https://github.com/freqtrade/freqtrade-strategies
|
||||||
|
for samples and inspiration.
|
||||||
|
"""
|
||||||
|
INTERFACE_VERSION = 2
|
||||||
|
stoploss = -0.10
|
||||||
|
timeframe = '5m'
|
||||||
|
startup_candle_count: int = 20
|
||||||
|
|
||||||
|
def informative_pairs(self):
|
||||||
|
return [('BTC/USDT', '5m')]
|
||||||
|
|
||||||
|
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe['buy'] = 0
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe['sell'] = 0
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
# Decorator stacking test.
|
||||||
|
@informative('30m')
|
||||||
|
@informative('1h')
|
||||||
|
def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe['rsi'] = 14
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
# Simple informative test.
|
||||||
|
@informative('1h', 'BTC/{stake}')
|
||||||
|
def populate_indicators_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe['rsi'] = 14
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
# Quote currency different from stake currency test.
|
||||||
|
@informative('1h', 'ETH/BTC')
|
||||||
|
def populate_indicators_eth_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe['rsi'] = 14
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
# Formatting test.
|
||||||
|
@informative('30m', 'BTC/{stake}', '{column}_{BASE}_{QUOTE}_{base}_{quote}_{asset}_{timeframe}')
|
||||||
|
def populate_indicators_btc_1h_2(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe['rsi'] = 14
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
# Custom formatter test
|
||||||
|
@informative('30m', 'ETH/{stake}', fmt=lambda column, **kwargs: column + '_from_callable')
|
||||||
|
def populate_indicators_eth_30m(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe['rsi'] = 14
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
# Strategy timeframe indicators for current pair.
|
||||||
|
dataframe['rsi'] = 14
|
||||||
|
# Informative pairs are available in this method.
|
||||||
|
dataframe['rsi_less'] = dataframe['rsi'] < dataframe['rsi_1h']
|
||||||
|
|
||||||
|
# Mixing manual informative pairs with decorators.
|
||||||
|
informative = self.dp.get_pair_dataframe('BTC/USDT', '5m')
|
||||||
|
informative['rsi'] = 14
|
||||||
|
dataframe = merge_informative_pair(dataframe, informative, self.timeframe, '5m', ffill=True)
|
||||||
|
|
||||||
|
return dataframe
|
@ -611,7 +611,7 @@ def test_is_informative_pairs_callback(default_conf):
|
|||||||
strategy = StrategyResolver.load_strategy(default_conf)
|
strategy = StrategyResolver.load_strategy(default_conf)
|
||||||
# Should return empty
|
# Should return empty
|
||||||
# Uses fallback to base implementation
|
# Uses fallback to base implementation
|
||||||
assert [] == strategy.informative_pairs()
|
assert [] == strategy.gather_informative_pairs()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('error', [
|
@pytest.mark.parametrize('error', [
|
||||||
|
@ -4,7 +4,9 @@ import numpy as np
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from freqtrade.strategy import merge_informative_pair, stoploss_from_open, timeframe_to_minutes
|
from freqtrade.data.dataprovider import DataProvider
|
||||||
|
from freqtrade.strategy import (merge_informative_pair, stoploss_from_absolute, stoploss_from_open,
|
||||||
|
timeframe_to_minutes)
|
||||||
|
|
||||||
|
|
||||||
def generate_test_data(timeframe: str, size: int):
|
def generate_test_data(timeframe: str, size: int):
|
||||||
@ -132,3 +134,65 @@ def test_stoploss_from_open():
|
|||||||
assert stoploss == 0
|
assert stoploss == 0
|
||||||
else:
|
else:
|
||||||
assert isclose(stop_price, expected_stop_price, rel_tol=0.00001)
|
assert isclose(stop_price, expected_stop_price, rel_tol=0.00001)
|
||||||
|
|
||||||
|
|
||||||
|
def test_stoploss_from_absolute():
|
||||||
|
assert stoploss_from_absolute(90, 100) == 1 - (90 / 100)
|
||||||
|
assert stoploss_from_absolute(100, 100) == 0
|
||||||
|
assert stoploss_from_absolute(110, 100) == 0
|
||||||
|
assert stoploss_from_absolute(100, 0) == 1
|
||||||
|
assert stoploss_from_absolute(0, 100) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_informative_decorator(mocker, default_conf):
|
||||||
|
test_data_5m = generate_test_data('5m', 40)
|
||||||
|
test_data_30m = generate_test_data('30m', 40)
|
||||||
|
test_data_1h = generate_test_data('1h', 40)
|
||||||
|
data = {
|
||||||
|
('XRP/USDT', '5m'): test_data_5m,
|
||||||
|
('XRP/USDT', '30m'): test_data_30m,
|
||||||
|
('XRP/USDT', '1h'): test_data_1h,
|
||||||
|
('LTC/USDT', '5m'): test_data_5m,
|
||||||
|
('LTC/USDT', '30m'): test_data_30m,
|
||||||
|
('LTC/USDT', '1h'): test_data_1h,
|
||||||
|
('BTC/USDT', '30m'): test_data_30m,
|
||||||
|
('BTC/USDT', '5m'): test_data_5m,
|
||||||
|
('BTC/USDT', '1h'): test_data_1h,
|
||||||
|
('ETH/USDT', '1h'): test_data_1h,
|
||||||
|
('ETH/USDT', '30m'): test_data_30m,
|
||||||
|
('ETH/BTC', '1h'): test_data_1h,
|
||||||
|
}
|
||||||
|
from .strats.informative_decorator_strategy import InformativeDecoratorTest
|
||||||
|
default_conf['stake_currency'] = 'USDT'
|
||||||
|
strategy = InformativeDecoratorTest(config=default_conf)
|
||||||
|
strategy.dp = DataProvider({}, None, None)
|
||||||
|
mocker.patch.object(strategy.dp, 'current_whitelist', return_value=[
|
||||||
|
'XRP/USDT', 'LTC/USDT', 'BTC/USDT'
|
||||||
|
])
|
||||||
|
|
||||||
|
assert len(strategy._ft_informative) == 6 # Equal to number of decorators used
|
||||||
|
informative_pairs = [('XRP/USDT', '1h'), ('LTC/USDT', '1h'), ('XRP/USDT', '30m'),
|
||||||
|
('LTC/USDT', '30m'), ('BTC/USDT', '1h'), ('BTC/USDT', '30m'),
|
||||||
|
('BTC/USDT', '5m'), ('ETH/BTC', '1h'), ('ETH/USDT', '30m')]
|
||||||
|
for inf_pair in informative_pairs:
|
||||||
|
assert inf_pair in strategy.gather_informative_pairs()
|
||||||
|
|
||||||
|
def test_historic_ohlcv(pair, timeframe):
|
||||||
|
return data[(pair, timeframe or strategy.timeframe)].copy()
|
||||||
|
mocker.patch('freqtrade.data.dataprovider.DataProvider.historic_ohlcv',
|
||||||
|
side_effect=test_historic_ohlcv)
|
||||||
|
|
||||||
|
analyzed = strategy.advise_all_indicators(
|
||||||
|
{p: data[(p, strategy.timeframe)] for p in ('XRP/USDT', 'LTC/USDT')})
|
||||||
|
expected_columns = [
|
||||||
|
'rsi_1h', 'rsi_30m', # Stacked informative decorators
|
||||||
|
'btc_usdt_rsi_1h', # BTC 1h informative
|
||||||
|
'rsi_BTC_USDT_btc_usdt_BTC/USDT_30m', # Column formatting
|
||||||
|
'rsi_from_callable', # Custom column formatter
|
||||||
|
'eth_btc_rsi_1h', # Quote currency not matching stake currency
|
||||||
|
'rsi', 'rsi_less', # Non-informative columns
|
||||||
|
'rsi_5m', # Manual informative dataframe
|
||||||
|
]
|
||||||
|
for _, dataframe in analyzed.items():
|
||||||
|
for col in expected_columns:
|
||||||
|
assert col in dataframe.columns
|
||||||
|
@ -35,7 +35,7 @@ def test_search_all_strategies_no_failed():
|
|||||||
directory = Path(__file__).parent / "strats"
|
directory = Path(__file__).parent / "strats"
|
||||||
strategies = StrategyResolver.search_all_objects(directory, enum_failed=False)
|
strategies = StrategyResolver.search_all_objects(directory, enum_failed=False)
|
||||||
assert isinstance(strategies, list)
|
assert isinstance(strategies, list)
|
||||||
assert len(strategies) == 3
|
assert len(strategies) == 4
|
||||||
assert isinstance(strategies[0], dict)
|
assert isinstance(strategies[0], dict)
|
||||||
|
|
||||||
|
|
||||||
@ -43,10 +43,10 @@ def test_search_all_strategies_with_failed():
|
|||||||
directory = Path(__file__).parent / "strats"
|
directory = Path(__file__).parent / "strats"
|
||||||
strategies = StrategyResolver.search_all_objects(directory, enum_failed=True)
|
strategies = StrategyResolver.search_all_objects(directory, enum_failed=True)
|
||||||
assert isinstance(strategies, list)
|
assert isinstance(strategies, list)
|
||||||
assert len(strategies) == 4
|
assert len(strategies) == 5
|
||||||
# with enum_failed=True search_all_objects() shall find 2 good strategies
|
# with enum_failed=True search_all_objects() shall find 2 good strategies
|
||||||
# and 1 which fails to load
|
# and 1 which fails to load
|
||||||
assert len([x for x in strategies if x['class'] is not None]) == 3
|
assert len([x for x in strategies if x['class'] is not None]) == 4
|
||||||
assert len([x for x in strategies if x['class'] is None]) == 1
|
assert len([x for x in strategies if x['class'] is None]) == 1
|
||||||
|
|
||||||
|
|
||||||
|
@ -78,11 +78,15 @@ def test_bot_cleanup(mocker, default_conf, caplog) -> None:
|
|||||||
assert coo_mock.call_count == 1
|
assert coo_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_order_dict_dry_run(default_conf, mocker, caplog) -> None:
|
@pytest.mark.parametrize('runmode', [
|
||||||
|
RunMode.DRY_RUN,
|
||||||
|
RunMode.LIVE
|
||||||
|
])
|
||||||
|
def test_order_dict(default_conf, mocker, runmode, caplog) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
conf = default_conf.copy()
|
conf = default_conf.copy()
|
||||||
conf['runmode'] = RunMode.DRY_RUN
|
conf['runmode'] = runmode
|
||||||
conf['order_types'] = {
|
conf['order_types'] = {
|
||||||
'buy': 'market',
|
'buy': 'market',
|
||||||
'sell': 'limit',
|
'sell': 'limit',
|
||||||
@ -92,45 +96,14 @@ def test_order_dict_dry_run(default_conf, mocker, caplog) -> None:
|
|||||||
conf['bid_strategy']['price_side'] = 'ask'
|
conf['bid_strategy']['price_side'] = 'ask'
|
||||||
|
|
||||||
freqtrade = FreqtradeBot(conf)
|
freqtrade = FreqtradeBot(conf)
|
||||||
|
if runmode == RunMode.LIVE:
|
||||||
|
assert not log_has_re(".*stoploss_on_exchange .* dry-run", caplog)
|
||||||
assert freqtrade.strategy.order_types['stoploss_on_exchange']
|
assert freqtrade.strategy.order_types['stoploss_on_exchange']
|
||||||
|
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
# is left untouched
|
# is left untouched
|
||||||
conf = default_conf.copy()
|
conf = default_conf.copy()
|
||||||
conf['runmode'] = RunMode.DRY_RUN
|
conf['runmode'] = runmode
|
||||||
conf['order_types'] = {
|
|
||||||
'buy': 'market',
|
|
||||||
'sell': 'limit',
|
|
||||||
'stoploss': 'limit',
|
|
||||||
'stoploss_on_exchange': False,
|
|
||||||
}
|
|
||||||
freqtrade = FreqtradeBot(conf)
|
|
||||||
assert not freqtrade.strategy.order_types['stoploss_on_exchange']
|
|
||||||
assert not log_has_re(".*stoploss_on_exchange .* dry-run", caplog)
|
|
||||||
|
|
||||||
|
|
||||||
def test_order_dict_live(default_conf, mocker, caplog) -> None:
|
|
||||||
patch_RPCManager(mocker)
|
|
||||||
patch_exchange(mocker)
|
|
||||||
|
|
||||||
conf = default_conf.copy()
|
|
||||||
conf['runmode'] = RunMode.LIVE
|
|
||||||
conf['order_types'] = {
|
|
||||||
'buy': 'market',
|
|
||||||
'sell': 'limit',
|
|
||||||
'stoploss': 'limit',
|
|
||||||
'stoploss_on_exchange': True,
|
|
||||||
}
|
|
||||||
conf['bid_strategy']['price_side'] = 'ask'
|
|
||||||
|
|
||||||
freqtrade = FreqtradeBot(conf)
|
|
||||||
assert not log_has_re(".*stoploss_on_exchange .* dry-run", caplog)
|
|
||||||
assert freqtrade.strategy.order_types['stoploss_on_exchange']
|
|
||||||
|
|
||||||
caplog.clear()
|
|
||||||
# is left untouched
|
|
||||||
conf = default_conf.copy()
|
|
||||||
conf['runmode'] = RunMode.LIVE
|
|
||||||
conf['order_types'] = {
|
conf['order_types'] = {
|
||||||
'buy': 'market',
|
'buy': 'market',
|
||||||
'sell': 'limit',
|
'sell': 'limit',
|
||||||
@ -219,8 +192,14 @@ def test_edge_overrides_stake_amount(mocker, edge_conf) -> None:
|
|||||||
'LTC/BTC', freqtrade.edge) == (999.9 * 0.5 * 0.01) / 0.21
|
'LTC/BTC', freqtrade.edge) == (999.9 * 0.5 * 0.01) / 0.21
|
||||||
|
|
||||||
|
|
||||||
def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf) -> None:
|
@pytest.mark.parametrize('buy_price_mult,ignore_strat_sl', [
|
||||||
|
# Override stoploss
|
||||||
|
(0.79, False),
|
||||||
|
# Override strategy stoploss
|
||||||
|
(0.85, True)
|
||||||
|
])
|
||||||
|
def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker,
|
||||||
|
buy_price_mult, ignore_strat_sl, edge_conf) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
patch_edge(mocker)
|
patch_edge(mocker)
|
||||||
@ -234,9 +213,9 @@ def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf
|
|||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
fetch_ticker=MagicMock(return_value={
|
fetch_ticker=MagicMock(return_value={
|
||||||
'bid': buy_price * 0.79,
|
'bid': buy_price * buy_price_mult,
|
||||||
'ask': buy_price * 0.79,
|
'ask': buy_price * buy_price_mult,
|
||||||
'last': buy_price * 0.79
|
'last': buy_price * buy_price_mult,
|
||||||
}),
|
}),
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
)
|
)
|
||||||
@ -253,46 +232,10 @@ def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf
|
|||||||
#############################################
|
#############################################
|
||||||
|
|
||||||
# stoploss shoud be hit
|
# stoploss shoud be hit
|
||||||
assert freqtrade.handle_trade(trade) is True
|
assert freqtrade.handle_trade(trade) is not ignore_strat_sl
|
||||||
assert log_has('Executing Sell for NEO/BTC. Reason: stop_loss', caplog)
|
if not ignore_strat_sl:
|
||||||
assert trade.sell_reason == SellType.STOP_LOSS.value
|
assert log_has('Executing Sell for NEO/BTC. Reason: stop_loss', caplog)
|
||||||
|
assert trade.sell_reason == SellType.STOP_LOSS.value
|
||||||
|
|
||||||
def test_edge_should_ignore_strategy_stoploss(limit_buy_order, fee,
|
|
||||||
mocker, edge_conf) -> None:
|
|
||||||
patch_RPCManager(mocker)
|
|
||||||
patch_exchange(mocker)
|
|
||||||
patch_edge(mocker)
|
|
||||||
edge_conf['max_open_trades'] = float('inf')
|
|
||||||
|
|
||||||
# Strategy stoploss is -0.1 but Edge imposes a stoploss at -0.2
|
|
||||||
# Thus, if price falls 15%, stoploss should not be triggered
|
|
||||||
#
|
|
||||||
# mocking the ticker: price is falling ...
|
|
||||||
buy_price = limit_buy_order['price']
|
|
||||||
mocker.patch.multiple(
|
|
||||||
'freqtrade.exchange.Exchange',
|
|
||||||
fetch_ticker=MagicMock(return_value={
|
|
||||||
'bid': buy_price * 0.85,
|
|
||||||
'ask': buy_price * 0.85,
|
|
||||||
'last': buy_price * 0.85
|
|
||||||
}),
|
|
||||||
get_fee=fee,
|
|
||||||
)
|
|
||||||
#############################################
|
|
||||||
|
|
||||||
# Create a trade with "limit_buy_order" price
|
|
||||||
freqtrade = FreqtradeBot(edge_conf)
|
|
||||||
freqtrade.active_pair_whitelist = ['NEO/BTC']
|
|
||||||
patch_get_signal(freqtrade)
|
|
||||||
freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
|
|
||||||
freqtrade.enter_positions()
|
|
||||||
trade = Trade.query.first()
|
|
||||||
trade.update(limit_buy_order)
|
|
||||||
#############################################
|
|
||||||
|
|
||||||
# stoploss shoud not be hit
|
|
||||||
assert freqtrade.handle_trade(trade) is False
|
|
||||||
|
|
||||||
|
|
||||||
def test_total_open_trades_stakes(mocker, default_conf, ticker, fee) -> None:
|
def test_total_open_trades_stakes(mocker, default_conf, ticker, fee) -> None:
|
||||||
@ -376,8 +319,16 @@ def test_create_trade_no_stake_amount(default_conf, ticker, limit_buy_order,
|
|||||||
freqtrade.create_trade('ETH/BTC')
|
freqtrade.create_trade('ETH/BTC')
|
||||||
|
|
||||||
|
|
||||||
def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order_open,
|
@pytest.mark.parametrize('stake_amount,create,amount_enough,max_open_trades', [
|
||||||
fee, mocker) -> None:
|
(0.0005, True, True, 99),
|
||||||
|
(0.000000005, True, False, 99),
|
||||||
|
(0, False, True, 99),
|
||||||
|
(UNLIMITED_STAKE_AMOUNT, False, True, 0),
|
||||||
|
])
|
||||||
|
def test_create_trade_minimal_amount(
|
||||||
|
default_conf, ticker, limit_buy_order_open, fee, mocker,
|
||||||
|
stake_amount, create, amount_enough, max_open_trades, caplog
|
||||||
|
) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
buy_mock = MagicMock(return_value=limit_buy_order_open)
|
buy_mock = MagicMock(return_value=limit_buy_order_open)
|
||||||
@ -387,78 +338,33 @@ def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order_open,
|
|||||||
create_order=buy_mock,
|
create_order=buy_mock,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
)
|
)
|
||||||
default_conf['stake_amount'] = 0.0005
|
default_conf['max_open_trades'] = max_open_trades
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
freqtrade.config['stake_amount'] = stake_amount
|
||||||
patch_get_signal(freqtrade)
|
patch_get_signal(freqtrade)
|
||||||
|
|
||||||
freqtrade.create_trade('ETH/BTC')
|
if create:
|
||||||
rate, amount = buy_mock.call_args[1]['rate'], buy_mock.call_args[1]['amount']
|
assert freqtrade.create_trade('ETH/BTC')
|
||||||
assert rate * amount <= default_conf['stake_amount']
|
if amount_enough:
|
||||||
|
rate, amount = buy_mock.call_args[1]['rate'], buy_mock.call_args[1]['amount']
|
||||||
|
assert rate * amount <= default_conf['stake_amount']
|
||||||
def test_create_trade_too_small_stake_amount(default_conf, ticker, limit_buy_order_open,
|
else:
|
||||||
fee, mocker, caplog) -> None:
|
assert log_has_re(
|
||||||
patch_RPCManager(mocker)
|
r"Stake amount for pair .* is too small.*",
|
||||||
patch_exchange(mocker)
|
caplog
|
||||||
buy_mock = MagicMock(return_value=limit_buy_order_open)
|
)
|
||||||
mocker.patch.multiple(
|
else:
|
||||||
'freqtrade.exchange.Exchange',
|
assert not freqtrade.create_trade('ETH/BTC')
|
||||||
fetch_ticker=ticker,
|
if not max_open_trades:
|
||||||
create_order=buy_mock,
|
assert freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.edge) == 0
|
||||||
get_fee=fee,
|
|
||||||
)
|
|
||||||
|
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
|
||||||
freqtrade.config['stake_amount'] = 0.000000005
|
|
||||||
|
|
||||||
patch_get_signal(freqtrade)
|
|
||||||
|
|
||||||
assert freqtrade.create_trade('ETH/BTC')
|
|
||||||
assert log_has_re(r"Stake amount for pair .* is too small.*", caplog)
|
|
||||||
|
|
||||||
|
|
||||||
def test_create_trade_zero_stake_amount(default_conf, ticker, limit_buy_order_open,
|
|
||||||
fee, mocker) -> None:
|
|
||||||
patch_RPCManager(mocker)
|
|
||||||
patch_exchange(mocker)
|
|
||||||
buy_mock = MagicMock(return_value=limit_buy_order_open)
|
|
||||||
mocker.patch.multiple(
|
|
||||||
'freqtrade.exchange.Exchange',
|
|
||||||
fetch_ticker=ticker,
|
|
||||||
create_order=buy_mock,
|
|
||||||
get_fee=fee,
|
|
||||||
)
|
|
||||||
|
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
|
||||||
freqtrade.config['stake_amount'] = 0
|
|
||||||
|
|
||||||
patch_get_signal(freqtrade)
|
|
||||||
|
|
||||||
assert not freqtrade.create_trade('ETH/BTC')
|
|
||||||
|
|
||||||
|
|
||||||
def test_create_trade_limit_reached(default_conf, ticker, limit_buy_order_open,
|
|
||||||
fee, mocker) -> None:
|
|
||||||
patch_RPCManager(mocker)
|
|
||||||
patch_exchange(mocker)
|
|
||||||
mocker.patch.multiple(
|
|
||||||
'freqtrade.exchange.Exchange',
|
|
||||||
fetch_ticker=ticker,
|
|
||||||
create_order=MagicMock(return_value=limit_buy_order_open),
|
|
||||||
get_fee=fee,
|
|
||||||
)
|
|
||||||
default_conf['max_open_trades'] = 0
|
|
||||||
default_conf['stake_amount'] = UNLIMITED_STAKE_AMOUNT
|
|
||||||
|
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
|
||||||
patch_get_signal(freqtrade)
|
|
||||||
|
|
||||||
assert not freqtrade.create_trade('ETH/BTC')
|
|
||||||
assert freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.edge) == 0
|
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('whitelist,positions', [
|
||||||
|
(["ETH/BTC"], 1), # No pairs left
|
||||||
|
([], 0), # No pairs in whitelist
|
||||||
|
])
|
||||||
def test_enter_positions_no_pairs_left(default_conf, ticker, limit_buy_order_open, fee,
|
def test_enter_positions_no_pairs_left(default_conf, ticker, limit_buy_order_open, fee,
|
||||||
mocker, caplog) -> None:
|
whitelist, positions, mocker, caplog) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@ -467,36 +373,20 @@ def test_enter_positions_no_pairs_left(default_conf, ticker, limit_buy_order_ope
|
|||||||
create_order=MagicMock(return_value=limit_buy_order_open),
|
create_order=MagicMock(return_value=limit_buy_order_open),
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
)
|
)
|
||||||
|
default_conf['exchange']['pair_whitelist'] = whitelist
|
||||||
default_conf['exchange']['pair_whitelist'] = ["ETH/BTC"]
|
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
patch_get_signal(freqtrade)
|
patch_get_signal(freqtrade)
|
||||||
|
|
||||||
n = freqtrade.enter_positions()
|
n = freqtrade.enter_positions()
|
||||||
assert n == 1
|
assert n == positions
|
||||||
assert not log_has_re(r"No currency pair in active pair whitelist.*", caplog)
|
if positions:
|
||||||
n = freqtrade.enter_positions()
|
assert not log_has_re(r"No currency pair in active pair whitelist.*", caplog)
|
||||||
assert n == 0
|
n = freqtrade.enter_positions()
|
||||||
assert log_has_re(r"No currency pair in active pair whitelist.*", caplog)
|
assert n == 0
|
||||||
|
assert log_has_re(r"No currency pair in active pair whitelist.*", caplog)
|
||||||
|
else:
|
||||||
def test_enter_positions_no_pairs_in_whitelist(default_conf, ticker, limit_buy_order, fee,
|
assert n == 0
|
||||||
mocker, caplog) -> None:
|
assert log_has("Active pair whitelist is empty.", caplog)
|
||||||
patch_RPCManager(mocker)
|
|
||||||
patch_exchange(mocker)
|
|
||||||
mocker.patch.multiple(
|
|
||||||
'freqtrade.exchange.Exchange',
|
|
||||||
fetch_ticker=ticker,
|
|
||||||
create_order=MagicMock(return_value={'id': limit_buy_order['id']}),
|
|
||||||
get_fee=fee,
|
|
||||||
)
|
|
||||||
default_conf['exchange']['pair_whitelist'] = []
|
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
|
||||||
patch_get_signal(freqtrade)
|
|
||||||
|
|
||||||
n = freqtrade.enter_positions()
|
|
||||||
assert n == 0
|
|
||||||
assert log_has("Active pair whitelist is empty.", caplog)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
@ -1668,30 +1558,27 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog,
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_enter_positions(mocker, default_conf, caplog) -> None:
|
@pytest.mark.parametrize('return_value,side_effect,log_message', [
|
||||||
|
(False, None, 'Found no buy signals for whitelisted currencies. Trying again...'),
|
||||||
|
(None, DependencyException, 'Unable to create trade for ETH/BTC: ')
|
||||||
|
])
|
||||||
|
def test_enter_positions(mocker, default_conf, return_value, side_effect,
|
||||||
|
log_message, caplog) -> None:
|
||||||
caplog.set_level(logging.DEBUG)
|
caplog.set_level(logging.DEBUG)
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
|
||||||
mock_ct = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.create_trade',
|
|
||||||
MagicMock(return_value=False))
|
|
||||||
n = freqtrade.enter_positions()
|
|
||||||
assert n == 0
|
|
||||||
assert log_has('Found no enter signals for whitelisted currencies. Trying again...', caplog)
|
|
||||||
# create_trade should be called once for every pair in the whitelist.
|
|
||||||
assert mock_ct.call_count == len(default_conf['exchange']['pair_whitelist'])
|
|
||||||
|
|
||||||
|
|
||||||
def test_enter_positions_exception(mocker, default_conf, caplog) -> None:
|
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
|
||||||
|
|
||||||
mock_ct = mocker.patch(
|
mock_ct = mocker.patch(
|
||||||
'freqtrade.freqtradebot.FreqtradeBot.create_trade',
|
'freqtrade.freqtradebot.FreqtradeBot.create_trade',
|
||||||
MagicMock(side_effect=DependencyException)
|
MagicMock(
|
||||||
|
return_value=return_value,
|
||||||
|
side_effect=side_effect
|
||||||
|
)
|
||||||
)
|
)
|
||||||
n = freqtrade.enter_positions()
|
n = freqtrade.enter_positions()
|
||||||
assert n == 0
|
assert n == 0
|
||||||
|
assert log_has(log_message, caplog)
|
||||||
|
# create_trade should be called once for every pair in the whitelist.
|
||||||
assert mock_ct.call_count == len(default_conf['exchange']['pair_whitelist'])
|
assert mock_ct.call_count == len(default_conf['exchange']['pair_whitelist'])
|
||||||
assert log_has('Unable to create trade for ETH/BTC: ', caplog)
|
|
||||||
|
|
||||||
|
|
||||||
def test_exit_positions(mocker, default_conf, limit_buy_order, caplog) -> None:
|
def test_exit_positions(mocker, default_conf, limit_buy_order, caplog) -> None:
|
||||||
@ -1785,8 +1672,13 @@ def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> No
|
|||||||
assert log_has_re('Found open order for.*', caplog)
|
assert log_has_re('Found open order for.*', caplog)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('initial_amount,has_rounding_fee', [
|
||||||
|
(90.99181073 + 1e-14, True),
|
||||||
|
(8.0, False)
|
||||||
|
])
|
||||||
def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_buy_order, fee,
|
def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_buy_order, fee,
|
||||||
mocker):
|
mocker, initial_amount, has_rounding_fee, caplog):
|
||||||
|
trades_for_order[0]['amount'] = initial_amount
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
|
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
|
||||||
# fetch_order should not be called!!
|
# fetch_order should not be called!!
|
||||||
mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError))
|
mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError))
|
||||||
@ -1807,32 +1699,8 @@ def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_
|
|||||||
freqtrade.update_trade_state(trade, '123456', limit_buy_order)
|
freqtrade.update_trade_state(trade, '123456', limit_buy_order)
|
||||||
assert trade.amount != amount
|
assert trade.amount != amount
|
||||||
assert trade.amount == limit_buy_order['amount']
|
assert trade.amount == limit_buy_order['amount']
|
||||||
|
if has_rounding_fee:
|
||||||
|
assert log_has_re(r'Applying fee on amount for .*', caplog)
|
||||||
def test_update_trade_state_withorderdict_rounding_fee(default_conf, trades_for_order, fee,
|
|
||||||
limit_buy_order, mocker, caplog):
|
|
||||||
trades_for_order[0]['amount'] = limit_buy_order['amount'] + 1e-14
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
|
|
||||||
# fetch_order should not be called!!
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError))
|
|
||||||
patch_exchange(mocker)
|
|
||||||
amount = sum(x['amount'] for x in trades_for_order)
|
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
|
||||||
trade = Trade(
|
|
||||||
pair='LTC/ETH',
|
|
||||||
amount=amount,
|
|
||||||
exchange='binance',
|
|
||||||
open_rate=0.245441,
|
|
||||||
fee_open=fee.return_value,
|
|
||||||
fee_close=fee.return_value,
|
|
||||||
open_order_id='123456',
|
|
||||||
is_open=True,
|
|
||||||
open_date=arrow.utcnow().datetime,
|
|
||||||
)
|
|
||||||
freqtrade.update_trade_state(trade, '123456', limit_buy_order)
|
|
||||||
assert trade.amount != amount
|
|
||||||
assert trade.amount == limit_buy_order['amount']
|
|
||||||
assert log_has_re(r'Applying fee on amount for .*', caplog)
|
|
||||||
|
|
||||||
|
|
||||||
def test_update_trade_state_exception(mocker, default_conf,
|
def test_update_trade_state_exception(mocker, default_conf,
|
||||||
@ -3144,16 +3012,28 @@ def test_execute_trade_exit_insufficient_funds_error(default_conf, ticker, fee,
|
|||||||
assert mock_insuf.call_count == 1
|
assert mock_insuf.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, limit_buy_order_open,
|
@pytest.mark.parametrize('profit_only,bid,ask,handle_first,handle_second,sell_type', [
|
||||||
fee, mocker) -> None:
|
# Enable profit
|
||||||
|
(True, 0.00001172, 0.00001173, False, True, SellType.SELL_SIGNAL.value),
|
||||||
|
# Disable profit
|
||||||
|
(False, 0.00002172, 0.00002173, True, False, SellType.SELL_SIGNAL.value),
|
||||||
|
# Enable loss
|
||||||
|
# * Shouldn't this be SellType.STOP_LOSS.value
|
||||||
|
(True, 0.00000172, 0.00000173, False, False, None),
|
||||||
|
# Disable loss
|
||||||
|
(False, 0.00000172, 0.00000173, True, False, SellType.SELL_SIGNAL.value),
|
||||||
|
])
|
||||||
|
def test_sell_profit_only(
|
||||||
|
default_conf, limit_buy_order, limit_buy_order_open,
|
||||||
|
fee, mocker, profit_only, bid, ask, handle_first, handle_second, sell_type) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
fetch_ticker=MagicMock(return_value={
|
fetch_ticker=MagicMock(return_value={
|
||||||
'bid': 0.00001172,
|
'bid': bid,
|
||||||
'ask': 0.00001173,
|
'ask': ask,
|
||||||
'last': 0.00001172
|
'last': bid
|
||||||
}),
|
}),
|
||||||
create_order=MagicMock(side_effect=[
|
create_order=MagicMock(side_effect=[
|
||||||
limit_buy_order_open,
|
limit_buy_order_open,
|
||||||
@ -3163,128 +3043,29 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, limit_buy
|
|||||||
)
|
)
|
||||||
default_conf.update({
|
default_conf.update({
|
||||||
'use_sell_signal': True,
|
'use_sell_signal': True,
|
||||||
'sell_profit_only': True,
|
'sell_profit_only': profit_only,
|
||||||
'sell_profit_offset': 0.1,
|
'sell_profit_offset': 0.1,
|
||||||
})
|
})
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
patch_get_signal(freqtrade)
|
patch_get_signal(freqtrade)
|
||||||
freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
|
if sell_type == SellType.SELL_SIGNAL.value:
|
||||||
|
freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
|
||||||
|
else:
|
||||||
|
freqtrade.strategy.stop_loss_reached = MagicMock(return_value=SellCheckTuple(
|
||||||
|
sell_type=SellType.NONE))
|
||||||
freqtrade.enter_positions()
|
freqtrade.enter_positions()
|
||||||
|
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
trade.update(limit_buy_order)
|
trade.update(limit_buy_order)
|
||||||
freqtrade.wallets.update()
|
freqtrade.wallets.update()
|
||||||
patch_get_signal(freqtrade, value=(False, True, None))
|
patch_get_signal(freqtrade, value=(False, True, None))
|
||||||
assert freqtrade.handle_trade(trade) is False
|
assert freqtrade.handle_trade(trade) is handle_first
|
||||||
|
|
||||||
freqtrade.strategy.sell_profit_offset = 0.0
|
if handle_second:
|
||||||
assert freqtrade.handle_trade(trade) is True
|
freqtrade.strategy.sell_profit_offset = 0.0
|
||||||
|
assert freqtrade.handle_trade(trade) is True
|
||||||
|
|
||||||
assert trade.sell_reason == SellType.SELL_SIGNAL.value
|
assert trade.sell_reason == sell_type
|
||||||
|
|
||||||
|
|
||||||
def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, limit_buy_order_open,
|
|
||||||
fee, mocker) -> None:
|
|
||||||
patch_RPCManager(mocker)
|
|
||||||
patch_exchange(mocker)
|
|
||||||
mocker.patch.multiple(
|
|
||||||
'freqtrade.exchange.Exchange',
|
|
||||||
fetch_ticker=MagicMock(return_value={
|
|
||||||
'bid': 0.00002172,
|
|
||||||
'ask': 0.00002173,
|
|
||||||
'last': 0.00002172
|
|
||||||
}),
|
|
||||||
create_order=MagicMock(side_effect=[
|
|
||||||
limit_buy_order_open,
|
|
||||||
{'id': 1234553382},
|
|
||||||
]),
|
|
||||||
get_fee=fee,
|
|
||||||
)
|
|
||||||
default_conf.update({
|
|
||||||
'use_sell_signal': True,
|
|
||||||
'sell_profit_only': False,
|
|
||||||
})
|
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
|
||||||
patch_get_signal(freqtrade)
|
|
||||||
freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
|
|
||||||
freqtrade.enter_positions()
|
|
||||||
|
|
||||||
trade = Trade.query.first()
|
|
||||||
trade.update(limit_buy_order)
|
|
||||||
freqtrade.wallets.update()
|
|
||||||
patch_get_signal(freqtrade, value=(False, True, None))
|
|
||||||
assert freqtrade.handle_trade(trade) is True
|
|
||||||
assert trade.sell_reason == SellType.SELL_SIGNAL.value
|
|
||||||
|
|
||||||
|
|
||||||
def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, limit_buy_order_open,
|
|
||||||
fee, mocker) -> None:
|
|
||||||
patch_RPCManager(mocker)
|
|
||||||
patch_exchange(mocker)
|
|
||||||
mocker.patch.multiple(
|
|
||||||
'freqtrade.exchange.Exchange',
|
|
||||||
fetch_ticker=MagicMock(return_value={
|
|
||||||
'bid': 0.00000172,
|
|
||||||
'ask': 0.00000173,
|
|
||||||
'last': 0.00000172
|
|
||||||
}),
|
|
||||||
create_order=MagicMock(side_effect=[
|
|
||||||
limit_buy_order_open,
|
|
||||||
{'id': 1234553382},
|
|
||||||
]),
|
|
||||||
get_fee=fee,
|
|
||||||
)
|
|
||||||
default_conf.update({
|
|
||||||
'use_sell_signal': True,
|
|
||||||
'sell_profit_only': True,
|
|
||||||
})
|
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
|
||||||
patch_get_signal(freqtrade)
|
|
||||||
freqtrade.strategy.stop_loss_reached = MagicMock(return_value=SellCheckTuple(
|
|
||||||
sell_type=SellType.NONE))
|
|
||||||
freqtrade.enter_positions()
|
|
||||||
|
|
||||||
trade = Trade.query.first()
|
|
||||||
trade.update(limit_buy_order)
|
|
||||||
patch_get_signal(freqtrade, value=(False, True, None))
|
|
||||||
assert freqtrade.handle_trade(trade) is False
|
|
||||||
|
|
||||||
|
|
||||||
def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, limit_buy_order_open,
|
|
||||||
fee, mocker) -> None:
|
|
||||||
patch_RPCManager(mocker)
|
|
||||||
patch_exchange(mocker)
|
|
||||||
mocker.patch.multiple(
|
|
||||||
'freqtrade.exchange.Exchange',
|
|
||||||
fetch_ticker=MagicMock(return_value={
|
|
||||||
'bid': 0.0000172,
|
|
||||||
'ask': 0.0000173,
|
|
||||||
'last': 0.0000172
|
|
||||||
}),
|
|
||||||
create_order=MagicMock(side_effect=[
|
|
||||||
limit_buy_order_open,
|
|
||||||
{'id': 1234553382},
|
|
||||||
]),
|
|
||||||
get_fee=fee,
|
|
||||||
)
|
|
||||||
default_conf.update({
|
|
||||||
'use_sell_signal': True,
|
|
||||||
'sell_profit_only': False,
|
|
||||||
})
|
|
||||||
|
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
|
||||||
patch_get_signal(freqtrade)
|
|
||||||
freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
|
|
||||||
|
|
||||||
freqtrade.enter_positions()
|
|
||||||
|
|
||||||
trade = Trade.query.first()
|
|
||||||
trade.update(limit_buy_order)
|
|
||||||
freqtrade.wallets.update()
|
|
||||||
patch_get_signal(freqtrade, value=(False, True, None))
|
|
||||||
assert freqtrade.handle_trade(trade) is True
|
|
||||||
assert trade.sell_reason == SellType.SELL_SIGNAL.value
|
|
||||||
|
|
||||||
|
|
||||||
def test_sell_not_enough_balance(default_conf, limit_buy_order, limit_buy_order_open,
|
def test_sell_not_enough_balance(default_conf, limit_buy_order, limit_buy_order_open,
|
||||||
@ -3322,11 +3103,15 @@ def test_sell_not_enough_balance(default_conf, limit_buy_order, limit_buy_order_
|
|||||||
assert trade.amount != amnt
|
assert trade.amount != amnt
|
||||||
|
|
||||||
|
|
||||||
def test__safe_exit_amount(default_conf, fee, caplog, mocker):
|
@pytest.mark.parametrize('amount_wallet,has_err', [
|
||||||
|
(95.29, False),
|
||||||
|
(91.29, True)
|
||||||
|
])
|
||||||
|
def test__safe_exit_amount(default_conf, fee, caplog, mocker, amount_wallet, has_err):
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
amount = 95.33
|
amount = 95.33
|
||||||
amount_wallet = 95.29
|
amount_wallet = amount_wallet
|
||||||
mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=amount_wallet))
|
mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=amount_wallet))
|
||||||
wallet_update = mocker.patch('freqtrade.wallets.Wallets.update')
|
wallet_update = mocker.patch('freqtrade.wallets.Wallets.update')
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
@ -3340,37 +3125,19 @@ def test__safe_exit_amount(default_conf, fee, caplog, mocker):
|
|||||||
)
|
)
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
patch_get_signal(freqtrade)
|
patch_get_signal(freqtrade)
|
||||||
|
if has_err:
|
||||||
wallet_update.reset_mock()
|
with pytest.raises(DependencyException, match=r"Not enough amount to sell."):
|
||||||
assert freqtrade._safe_exit_amount(trade.pair, trade.amount) == amount_wallet
|
assert freqtrade._safe_exit_amount(trade.pair, trade.amount)
|
||||||
assert log_has_re(r'.*Falling back to wallet-amount.', caplog)
|
else:
|
||||||
assert wallet_update.call_count == 1
|
wallet_update.reset_mock()
|
||||||
caplog.clear()
|
assert freqtrade._safe_exit_amount(trade.pair, trade.amount) == amount_wallet
|
||||||
wallet_update.reset_mock()
|
assert log_has_re(r'.*Falling back to wallet-amount.', caplog)
|
||||||
assert freqtrade._safe_exit_amount(trade.pair, amount_wallet) == amount_wallet
|
assert wallet_update.call_count == 1
|
||||||
assert not log_has_re(r'.*Falling back to wallet-amount.', caplog)
|
caplog.clear()
|
||||||
assert wallet_update.call_count == 1
|
wallet_update.reset_mock()
|
||||||
|
assert freqtrade._safe_exit_amount(trade.pair, amount_wallet) == amount_wallet
|
||||||
|
assert not log_has_re(r'.*Falling back to wallet-amount.', caplog)
|
||||||
def test__safe_exit_amount_error(default_conf, fee, caplog, mocker):
|
assert wallet_update.call_count == 1
|
||||||
patch_RPCManager(mocker)
|
|
||||||
patch_exchange(mocker)
|
|
||||||
amount = 95.33
|
|
||||||
amount_wallet = 91.29
|
|
||||||
mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=amount_wallet))
|
|
||||||
trade = Trade(
|
|
||||||
pair='LTC/ETH',
|
|
||||||
amount=amount,
|
|
||||||
exchange='binance',
|
|
||||||
open_rate=0.245441,
|
|
||||||
open_order_id="123456",
|
|
||||||
fee_open=fee.return_value,
|
|
||||||
fee_close=fee.return_value,
|
|
||||||
)
|
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
|
||||||
patch_get_signal(freqtrade)
|
|
||||||
with pytest.raises(DependencyException, match=r"Not enough amount to exit."):
|
|
||||||
assert freqtrade._safe_exit_amount(trade.pair, trade.amount)
|
|
||||||
|
|
||||||
|
|
||||||
def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplog) -> None:
|
def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplog) -> None:
|
||||||
@ -4158,50 +3925,37 @@ def test_order_book_depth_of_market_high_delta(default_conf, ticker, limit_buy_o
|
|||||||
assert trade is None
|
assert trade is None
|
||||||
|
|
||||||
|
|
||||||
def test_order_book_bid_strategy1(mocker, default_conf, order_book_l2) -> None:
|
@pytest.mark.parametrize('exception_thrown,ask,last,order_book_top,order_book', [
|
||||||
|
(False, 0.045, 0.046, 2, None),
|
||||||
|
(True, 0.042, 0.046, 1, {'bids': [[]], 'asks': [[]]})
|
||||||
|
])
|
||||||
|
def test_order_book_bid_strategy1(mocker, default_conf, order_book_l2, exception_thrown,
|
||||||
|
ask, last, order_book_top, order_book, caplog) -> None:
|
||||||
"""
|
"""
|
||||||
test if function get_rate will return the order book price
|
test if function get_rate will return the order book price instead of the ask rate
|
||||||
instead of the ask rate
|
|
||||||
"""
|
"""
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
ticker_mock = MagicMock(return_value={'ask': 0.045, 'last': 0.046})
|
ticker_mock = MagicMock(return_value={'ask': ask, 'last': last})
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
fetch_l2_order_book=order_book_l2,
|
fetch_l2_order_book=MagicMock(return_value=order_book) if order_book else order_book_l2,
|
||||||
fetch_ticker=ticker_mock,
|
fetch_ticker=ticker_mock,
|
||||||
|
|
||||||
)
|
)
|
||||||
default_conf['exchange']['name'] = 'binance'
|
default_conf['exchange']['name'] = 'binance'
|
||||||
default_conf['bid_strategy']['use_order_book'] = True
|
default_conf['bid_strategy']['use_order_book'] = True
|
||||||
default_conf['bid_strategy']['order_book_top'] = 2
|
default_conf['bid_strategy']['order_book_top'] = order_book_top
|
||||||
default_conf['bid_strategy']['ask_last_balance'] = 0
|
default_conf['bid_strategy']['ask_last_balance'] = 0
|
||||||
default_conf['telegram']['enabled'] = False
|
default_conf['telegram']['enabled'] = False
|
||||||
|
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
assert freqtrade.exchange.get_rate('ETH/BTC', refresh=True, side="buy") == 0.043935
|
if exception_thrown:
|
||||||
assert ticker_mock.call_count == 0
|
with pytest.raises(PricingError):
|
||||||
|
freqtrade.exchange.get_rate('ETH/BTC', refresh=True, side="buy")
|
||||||
|
assert log_has_re(
|
||||||
def test_order_book_bid_strategy_exception(mocker, default_conf, caplog) -> None:
|
r'Buy Price at location 1 from orderbook could not be determined.', caplog)
|
||||||
patch_exchange(mocker)
|
else:
|
||||||
ticker_mock = MagicMock(return_value={'ask': 0.042, 'last': 0.046})
|
assert freqtrade.exchange.get_rate('ETH/BTC', refresh=True, side="buy") == 0.043935
|
||||||
mocker.patch.multiple(
|
assert ticker_mock.call_count == 0
|
||||||
'freqtrade.exchange.Exchange',
|
|
||||||
fetch_l2_order_book=MagicMock(return_value={'bids': [[]], 'asks': [[]]}),
|
|
||||||
fetch_ticker=ticker_mock,
|
|
||||||
|
|
||||||
)
|
|
||||||
default_conf['exchange']['name'] = 'binance'
|
|
||||||
default_conf['bid_strategy']['use_order_book'] = True
|
|
||||||
default_conf['bid_strategy']['order_book_top'] = 1
|
|
||||||
default_conf['bid_strategy']['ask_last_balance'] = 0
|
|
||||||
default_conf['telegram']['enabled'] = False
|
|
||||||
|
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
|
||||||
# orderbook shall be used even if tickers would be lower.
|
|
||||||
with pytest.raises(PricingError):
|
|
||||||
freqtrade.exchange.get_rate('ETH/BTC', refresh=True, side="buy")
|
|
||||||
assert log_has_re(r'Buy Price at location 1 from orderbook could not be determined.', caplog)
|
|
||||||
|
|
||||||
|
|
||||||
def test_check_depth_of_market_buy(default_conf, mocker, order_book_l2) -> None:
|
def test_check_depth_of_market_buy(default_conf, mocker, order_book_l2) -> None:
|
||||||
|
Loading…
Reference in New Issue
Block a user