This commit is contained in:
longyu 2022-08-20 16:21:34 +02:00
commit 3110b869f7
38 changed files with 1677 additions and 676 deletions

BIN
docs/assets/freqai_DI.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 995 KiB

After

Width:  |  Height:  |  Size: 17 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 MiB

View File

@ -374,6 +374,7 @@ usage: freqtrade list-data [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
[--data-format-ohlcv {json,jsongz,hdf5}] [--data-format-ohlcv {json,jsongz,hdf5}]
[-p PAIRS [PAIRS ...]] [-p PAIRS [PAIRS ...]]
[--trading-mode {spot,margin,futures}] [--trading-mode {spot,margin,futures}]
[--show-timerange]
optional arguments: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
@ -387,6 +388,8 @@ optional arguments:
separated. separated.
--trading-mode {spot,margin,futures} --trading-mode {spot,margin,futures}
Select Trading mode Select Trading mode
--show-timerange Show timerange available for available data. (May take
a while to calculate).
Common arguments: Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages). -v, --verbose Verbose mode (-vv for more, -vvv to get all messages).

File diff suppressed because it is too large Load Diff

View File

@ -14,7 +14,7 @@ from freqtrade.configuration import Configuration
# Initialize empty configuration object # Initialize empty configuration object
config = Configuration.from_files([]) config = Configuration.from_files([])
# Optionally, use existing configuration file # Optionally (recommended), use existing configuration file
# config = Configuration.from_files(["config.json"]) # config = Configuration.from_files(["config.json"])
# Define some constants # Define some constants
@ -22,7 +22,7 @@ config["timeframe"] = "5m"
# Name of the strategy class # Name of the strategy class
config["strategy"] = "SampleStrategy" config["strategy"] = "SampleStrategy"
# Location of the data # Location of the data
data_location = Path(config['user_data_dir'], 'data', 'binance') data_location = config['datadir']
# Pair to analyze - Only use one pair here # Pair to analyze - Only use one pair here
pair = "BTC/USDT" pair = "BTC/USDT"
``` ```

View File

@ -611,6 +611,26 @@ Common arguments:
``` ```
### Webserver mode - docker
You can also use webserver mode via docker.
Starting a one-off container requires the configuration of the port explicitly, as ports are not exposed by default.
You can use `docker-compose run --rm -p 127.0.0.1:8080:8080 freqtrade webserver` to start a one-off container that'll be removed once you stop it. This assumes that port 8080 is still available and no other bot is running on that port.
Alternatively, you can reconfigure the docker-compose file to have the command updated:
``` yml
command: >
webserver
--config /freqtrade/user_data/config.json
```
You can now use `docker-compose up` to start the webserver.
This assumes that the configuration has a webserver enabled and configured for docker (listening port = `0.0.0.0`).
!!! Tip
Don't forget to reset the command back to the trade command if you want to start a live or dry-run bot.
## Show previous Backtest results ## Show previous Backtest results
Allows you to show previous backtest results. Allows you to show previous backtest results.

View File

@ -69,7 +69,7 @@ ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes", "exchange", "tradin
ARGS_CONVERT_TRADES = ["pairs", "timeframes", "exchange", "dataformat_ohlcv", "dataformat_trades"] ARGS_CONVERT_TRADES = ["pairs", "timeframes", "exchange", "dataformat_ohlcv", "dataformat_trades"]
ARGS_LIST_DATA = ["exchange", "dataformat_ohlcv", "pairs", "trading_mode"] ARGS_LIST_DATA = ["exchange", "dataformat_ohlcv", "pairs", "trading_mode", "show_timerange"]
ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "new_pairs_days", "include_inactive", ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "new_pairs_days", "include_inactive",
"timerange", "download_trades", "exchange", "timeframes", "timerange", "download_trades", "exchange", "timeframes",

View File

@ -367,7 +367,7 @@ AVAILABLE_CLI_OPTIONS = {
metavar='BASE_CURRENCY', metavar='BASE_CURRENCY',
), ),
"trading_mode": Arg( "trading_mode": Arg(
'--trading-mode', '--trading-mode', '--tradingmode',
help='Select Trading mode', help='Select Trading mode',
choices=constants.TRADING_MODES, choices=constants.TRADING_MODES,
), ),
@ -434,6 +434,11 @@ AVAILABLE_CLI_OPTIONS = {
help='Storage format for downloaded trades data. (default: `jsongz`).', help='Storage format for downloaded trades data. (default: `jsongz`).',
choices=constants.AVAILABLE_DATAHANDLERS, choices=constants.AVAILABLE_DATAHANDLERS,
), ),
"show_timerange": Arg(
'--show-timerange',
help='Show timerange available for available data. (May take a while to calculate).',
action='store_true',
),
"exchange": Arg( "exchange": Arg(
'--exchange', '--exchange',
help=f'Exchange name (default: `{constants.DEFAULT_EXCHANGE}`). ' help=f'Exchange name (default: `{constants.DEFAULT_EXCHANGE}`). '

View File

@ -5,6 +5,7 @@ from datetime import datetime, timedelta
from typing import Any, Dict, List from typing import Any, Dict, List
from freqtrade.configuration import TimeRange, setup_utils_configuration from freqtrade.configuration import TimeRange, setup_utils_configuration
from freqtrade.constants import DATETIME_PRINT_FORMAT
from freqtrade.data.converter import convert_ohlcv_format, convert_trades_format from freqtrade.data.converter import convert_ohlcv_format, convert_trades_format
from freqtrade.data.history import (convert_trades_to_ohlcv, refresh_backtest_ohlcv_data, from freqtrade.data.history import (convert_trades_to_ohlcv, refresh_backtest_ohlcv_data,
refresh_backtest_trades_data) refresh_backtest_trades_data)
@ -177,17 +178,31 @@ def start_list_data(args: Dict[str, Any]) -> None:
paircombs = [comb for comb in paircombs if comb[0] in args['pairs']] paircombs = [comb for comb in paircombs if comb[0] in args['pairs']]
print(f"Found {len(paircombs)} pair / timeframe combinations.") print(f"Found {len(paircombs)} pair / timeframe combinations.")
groupedpair = defaultdict(list) if not config.get('show_timerange'):
for pair, timeframe, candle_type in sorted( groupedpair = defaultdict(list)
paircombs, for pair, timeframe, candle_type in sorted(
key=lambda x: (x[0], timeframe_to_minutes(x[1]), x[2]) paircombs,
): key=lambda x: (x[0], timeframe_to_minutes(x[1]), x[2])
groupedpair[(pair, candle_type)].append(timeframe) ):
groupedpair[(pair, candle_type)].append(timeframe)
if groupedpair: if groupedpair:
print(tabulate([
(pair, ', '.join(timeframes), candle_type)
for (pair, candle_type), timeframes in groupedpair.items()
],
headers=("Pair", "Timeframe", "Type"),
tablefmt='psql', stralign='right'))
else:
paircombs1 = [(
pair, timeframe, candle_type,
*dhc.ohlcv_data_min_max(pair, timeframe, candle_type)
) for pair, timeframe, candle_type in paircombs]
print(tabulate([ print(tabulate([
(pair, ', '.join(timeframes), candle_type) (pair, timeframe, candle_type,
for (pair, candle_type), timeframes in groupedpair.items() start.strftime(DATETIME_PRINT_FORMAT),
], end.strftime(DATETIME_PRINT_FORMAT))
headers=("Pair", "Timeframe", "Type"), for pair, timeframe, candle_type, start, end in paircombs1
],
headers=("Pair", "Timeframe", "Type", 'From', 'To'),
tablefmt='psql', stralign='right')) tablefmt='psql', stralign='right'))

View File

@ -426,6 +426,9 @@ class Configuration:
self._args_to_config(config, argname='dataformat_trades', self._args_to_config(config, argname='dataformat_trades',
logstring='Using "{}" to store trades data.') logstring='Using "{}" to store trades data.')
self._args_to_config(config, argname='show_timerange',
logstring='Detected --show-timerange')
def _process_data_options(self, config: Dict[str, Any]) -> None: def _process_data_options(self, config: Dict[str, Any]) -> None:
self._args_to_config(config, argname='new_pairs_days', self._args_to_config(config, argname='new_pairs_days',
logstring='Detected --new-pairs-days: {}') logstring='Detected --new-pairs-days: {}')

View File

@ -7,9 +7,8 @@ import numpy as np
import pandas as pd import pandas as pd
from freqtrade.configuration import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.constants import (DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, TradeList
ListPairsWithTimeframes, TradeList) from freqtrade.enums import CandleType
from freqtrade.enums import CandleType, TradingMode
from .idatahandler import IDataHandler from .idatahandler import IDataHandler
@ -21,29 +20,6 @@ class HDF5DataHandler(IDataHandler):
_columns = DEFAULT_DATAFRAME_COLUMNS _columns = DEFAULT_DATAFRAME_COLUMNS
@classmethod
def ohlcv_get_available_data(
cls, datadir: Path, trading_mode: TradingMode) -> ListPairsWithTimeframes:
"""
Returns a list of all pairs with ohlcv data available in this datadir
:param datadir: Directory to search for ohlcv files
:param trading_mode: trading-mode to be used
:return: List of Tuples of (pair, timeframe)
"""
if trading_mode == TradingMode.FUTURES:
datadir = datadir.joinpath('futures')
_tmp = [
re.search(
cls._OHLCV_REGEX, p.name
) for p in datadir.glob("*.h5")
]
return [
(
cls.rebuild_pair_from_filename(match[1]),
cls.rebuild_timeframe_from_filename(match[2]),
CandleType.from_string(match[3])
) for match in _tmp if match and len(match.groups()) > 1]
@classmethod @classmethod
def ohlcv_get_pairs(cls, datadir: Path, timeframe: str, candle_type: CandleType) -> List[str]: def ohlcv_get_pairs(cls, datadir: Path, timeframe: str, candle_type: CandleType) -> List[str]:
""" """

View File

@ -56,7 +56,7 @@ def load_pair_history(pair: str,
fill_missing=fill_up_missing, fill_missing=fill_up_missing,
drop_incomplete=drop_incomplete, drop_incomplete=drop_incomplete,
startup_candles=startup_candles, startup_candles=startup_candles,
candle_type=candle_type candle_type=candle_type,
) )
@ -97,14 +97,15 @@ def load_data(datadir: Path,
fill_up_missing=fill_up_missing, fill_up_missing=fill_up_missing,
startup_candles=startup_candles, startup_candles=startup_candles,
data_handler=data_handler, data_handler=data_handler,
candle_type=candle_type candle_type=candle_type,
) )
if not hist.empty: if not hist.empty:
result[pair] = hist result[pair] = hist
else: else:
if candle_type is CandleType.FUNDING_RATE and user_futures_funding_rate is not None: if candle_type is CandleType.FUNDING_RATE and user_futures_funding_rate is not None:
logger.warn(f"{pair} using user specified [{user_futures_funding_rate}]") logger.warn(f"{pair} using user specified [{user_futures_funding_rate}]")
result[pair] = DataFrame(columns=["open", "close", "high", "low", "volume"]) elif candle_type not in (CandleType.SPOT, CandleType.FUTURES):
result[pair] = DataFrame(columns=["date", "open", "close", "high", "low", "volume"])
if fail_without_data and not result: if fail_without_data and not result:
raise OperationalException("No data found. Terminating.") raise OperationalException("No data found. Terminating.")

View File

@ -9,7 +9,7 @@ from abc import ABC, abstractmethod
from copy import deepcopy from copy import deepcopy
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from typing import List, Optional, Type from typing import List, Optional, Tuple, Type
from pandas import DataFrame from pandas import DataFrame
@ -39,15 +39,26 @@ class IDataHandler(ABC):
raise NotImplementedError() raise NotImplementedError()
@classmethod @classmethod
@abstractmethod
def ohlcv_get_available_data( def ohlcv_get_available_data(
cls, datadir: Path, trading_mode: TradingMode) -> ListPairsWithTimeframes: cls, datadir: Path, trading_mode: TradingMode) -> ListPairsWithTimeframes:
""" """
Returns a list of all pairs with ohlcv data available in this datadir Returns a list of all pairs with ohlcv data available in this datadir
:param datadir: Directory to search for ohlcv files :param datadir: Directory to search for ohlcv files
:param trading_mode: trading-mode to be used :param trading_mode: trading-mode to be used
:return: List of Tuples of (pair, timeframe) :return: List of Tuples of (pair, timeframe, CandleType)
""" """
if trading_mode == TradingMode.FUTURES:
datadir = datadir.joinpath('futures')
_tmp = [
re.search(
cls._OHLCV_REGEX, p.name
) for p in datadir.glob(f"*.{cls._get_file_extension()}")]
return [
(
cls.rebuild_pair_from_filename(match[1]),
cls.rebuild_timeframe_from_filename(match[2]),
CandleType.from_string(match[3])
) for match in _tmp if match and len(match.groups()) > 1]
@classmethod @classmethod
@abstractmethod @abstractmethod
@ -73,6 +84,18 @@ class IDataHandler(ABC):
:return: None :return: None
""" """
def ohlcv_data_min_max(self, pair: str, timeframe: str,
candle_type: CandleType) -> Tuple[datetime, datetime]:
"""
Returns the min and max timestamp for the given pair and timeframe.
:param pair: Pair to get min/max for
:param timeframe: Timeframe to get min/max for
:param candle_type: Any of the enum CandleType (must match trading mode!)
:return: (min, max)
"""
data = self._ohlcv_load(pair, timeframe, None, candle_type)
return data.iloc[0]['date'].to_pydatetime(), data.iloc[-1]['date'].to_pydatetime()
@abstractmethod @abstractmethod
def _ohlcv_load(self, pair: str, timeframe: str, timerange: Optional[TimeRange], def _ohlcv_load(self, pair: str, timeframe: str, timerange: Optional[TimeRange],
candle_type: CandleType candle_type: CandleType

View File

@ -8,9 +8,9 @@ from pandas import DataFrame, read_json, to_datetime
from freqtrade import misc from freqtrade import misc
from freqtrade.configuration import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, ListPairsWithTimeframes, TradeList from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, TradeList
from freqtrade.data.converter import trades_dict_to_list from freqtrade.data.converter import trades_dict_to_list
from freqtrade.enums import CandleType, TradingMode from freqtrade.enums import CandleType
from .idatahandler import IDataHandler from .idatahandler import IDataHandler
@ -23,28 +23,6 @@ class JsonDataHandler(IDataHandler):
_use_zip = False _use_zip = False
_columns = DEFAULT_DATAFRAME_COLUMNS _columns = DEFAULT_DATAFRAME_COLUMNS
@classmethod
def ohlcv_get_available_data(
cls, datadir: Path, trading_mode: TradingMode) -> ListPairsWithTimeframes:
"""
Returns a list of all pairs with ohlcv data available in this datadir
:param datadir: Directory to search for ohlcv files
:param trading_mode: trading-mode to be used
:return: List of Tuples of (pair, timeframe)
"""
if trading_mode == 'futures':
datadir = datadir.joinpath('futures')
_tmp = [
re.search(
cls._OHLCV_REGEX, p.name
) for p in datadir.glob(f"*.{cls._get_file_extension()}")]
return [
(
cls.rebuild_pair_from_filename(match[1]),
cls.rebuild_timeframe_from_filename(match[2]),
CandleType.from_string(match[3])
) for match in _tmp if match and len(match.groups()) > 1]
@classmethod @classmethod
def ohlcv_get_pairs(cls, datadir: Path, timeframe: str, candle_type: CandleType) -> List[str]: def ohlcv_get_pairs(cls, datadir: Path, timeframe: str, candle_type: CandleType) -> List[str]:
""" """

File diff suppressed because it is too large Load Diff

View File

@ -17,6 +17,7 @@ import ccxt
import ccxt.async_support as ccxt_async import ccxt.async_support as ccxt_async
from cachetools import TTLCache from cachetools import TTLCache
from ccxt import ROUND_DOWN, ROUND_UP, TICK_SIZE, TRUNCATE, decimal_to_precision from ccxt import ROUND_DOWN, ROUND_UP, TICK_SIZE, TRUNCATE, decimal_to_precision
from dateutil import parser
from pandas import DataFrame from pandas import DataFrame
from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES, BuySell, from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES, BuySell,
@ -30,7 +31,8 @@ from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, BAD_EXCHANGE
EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED, EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED,
SUPPORTED_EXCHANGES, remove_credentials, retrier, SUPPORTED_EXCHANGES, remove_credentials, retrier,
retrier_async) retrier_async)
from freqtrade.misc import chunks, deep_merge_dicts, safe_value_fallback2 from freqtrade.misc import (chunks, deep_merge_dicts, file_dump_json, file_load_json,
safe_value_fallback2)
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
from freqtrade.util import FtPrecise from freqtrade.util import FtPrecise
@ -2207,6 +2209,7 @@ class Exchange:
@retrier_async @retrier_async
async def get_market_leverage_tiers(self, symbol: str) -> Tuple[str, List[Dict]]: async def get_market_leverage_tiers(self, symbol: str) -> Tuple[str, List[Dict]]:
""" Leverage tiers per symbol """
try: try:
tier = await self._api_async.fetch_market_leverage_tiers(symbol) tier = await self._api_async.fetch_market_leverage_tiers(symbol)
return symbol, tier return symbol, tier
@ -2238,12 +2241,21 @@ class Exchange:
tiers: Dict[str, List[Dict]] = {} tiers: Dict[str, List[Dict]] = {}
# Be verbose here, as this delays startup by ~1 minute. tiers_cached = self.load_cached_leverage_tiers(self._config['stake_currency'])
logger.info( if tiers_cached:
f"Initializing leverage_tiers for {len(symbols)} markets. " tiers = tiers_cached
"This will take about a minute.")
coros = [self.get_market_leverage_tiers(symbol) for symbol in sorted(symbols)] coros = [
self.get_market_leverage_tiers(symbol)
for symbol in sorted(symbols) if symbol not in tiers]
# Be verbose here, as this delays startup by ~1 minute.
if coros:
logger.info(
f"Initializing leverage_tiers for {len(symbols)} markets. "
"This will take about a minute.")
else:
logger.info("Using cached leverage_tiers.")
async def gather_results(): async def gather_results():
return await asyncio.gather(*input_coro, return_exceptions=True) return await asyncio.gather(*input_coro, return_exceptions=True)
@ -2255,7 +2267,8 @@ class Exchange:
for symbol, res in results: for symbol, res in results:
tiers[symbol] = res tiers[symbol] = res
if len(coros) > 0:
self.cache_leverage_tiers(tiers, self._config['stake_currency'])
logger.info(f"Done initializing {len(symbols)} markets.") logger.info(f"Done initializing {len(symbols)} markets.")
return tiers return tiers
@ -2264,6 +2277,30 @@ class Exchange:
else: else:
return {} return {}
def cache_leverage_tiers(self, tiers: Dict[str, List[Dict]], stake_currency: str) -> None:
filename = self._config['datadir'] / "futures" / f"leverage_tiers_{stake_currency}.json"
if not filename.parent.is_dir():
filename.parent.mkdir(parents=True)
data = {
"updated": datetime.now(timezone.utc),
"data": tiers,
}
file_dump_json(filename, data)
def load_cached_leverage_tiers(self, stake_currency: str) -> Optional[Dict[str, List[Dict]]]:
filename = self._config['datadir'] / "futures" / f"leverage_tiers_{stake_currency}.json"
if filename.is_file():
tiers = file_load_json(filename)
updated = tiers.get('updated')
if updated:
updated_dt = parser.parse(updated)
if updated_dt < datetime.now(timezone.utc) - timedelta(days=1):
logger.info("Cached leverage tiers are outdated. Will update.")
return None
return tiers['data']
return None
def fill_leverage_tiers(self) -> None: def fill_leverage_tiers(self) -> None:
""" """
Assigns property _leverage_tiers to a dictionary of information about the leverage Assigns property _leverage_tiers to a dictionary of information about the leverage
@ -2377,7 +2414,8 @@ class Exchange:
return return
try: try:
self._api.set_leverage(symbol=pair, leverage=leverage) res = self._api.set_leverage(symbol=pair, leverage=leverage)
self._log_exchange_response('set_leverage', res)
except ccxt.DDoSProtection as e: except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
@ -2405,7 +2443,6 @@ class Exchange:
if self.trading_mode in TradingMode.SPOT: if self.trading_mode in TradingMode.SPOT:
return None return None
elif ( elif (
self.margin_mode == MarginMode.ISOLATED and
self.trading_mode == TradingMode.FUTURES self.trading_mode == TradingMode.FUTURES
): ):
wallet_balance = (amount * open_rate) / leverage wallet_balance = (amount * open_rate) / leverage
@ -2421,7 +2458,7 @@ class Exchange:
return isolated_liq return isolated_liq
else: else:
raise OperationalException( raise OperationalException(
"Freqtrade only supports isolated futures for leverage trading") "Freqtrade currently only supports futures for leverage trading.")
def funding_fee_cutoff(self, open_date: datetime): def funding_fee_cutoff(self, open_date: datetime):
""" """
@ -2441,7 +2478,8 @@ class Exchange:
return return
try: try:
self._api.set_margin_mode(margin_mode.value, pair, params) res = self._api.set_margin_mode(margin_mode.value, pair, params)
self._log_exchange_response('set_margin_mode', res)
except ccxt.DDoSProtection as e: except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
@ -2599,7 +2637,7 @@ class Exchange:
""" """
if self.trading_mode == TradingMode.SPOT: if self.trading_mode == TradingMode.SPOT:
return None return None
elif (self.trading_mode != TradingMode.FUTURES and self.margin_mode != MarginMode.ISOLATED): elif (self.trading_mode != TradingMode.FUTURES):
raise OperationalException( raise OperationalException(
f"{self.name} does not support {self.margin_mode.value} {self.trading_mode.value}") f"{self.name} does not support {self.margin_mode.value} {self.trading_mode.value}")

View File

@ -34,6 +34,7 @@ class Gateio(Exchange):
_ft_has_futures: Dict = { _ft_has_futures: Dict = {
"needs_trading_fees": True, "needs_trading_fees": True,
"ohlcv_volume_currency": "base",
"fee_cost_in_contracts": False, # Set explicitly to false for clarity "fee_cost_in_contracts": False, # Set explicitly to false for clarity
"order_props_in_contracts": ['amount', 'filled', 'remaining'], "order_props_in_contracts": ['amount', 'filled', 'remaining'],
} }

View File

@ -511,7 +511,6 @@ class FreqaiDataKitchen:
pairwise = pairwise.reshape(-1, 1) pairwise = pairwise.reshape(-1, 1)
avg_mean_dist = pairwise[~np.isnan(pairwise)].mean() avg_mean_dist = pairwise[~np.isnan(pairwise)].mean()
return avg_mean_dist return avg_mean_dist
def use_SVM_to_remove_outliers(self, predict: bool) -> None: def use_SVM_to_remove_outliers(self, predict: bool) -> None:

View File

@ -418,7 +418,7 @@ class FreqtradeBot(LoggingMixin):
whitelist = copy.deepcopy(self.active_pair_whitelist) whitelist = copy.deepcopy(self.active_pair_whitelist)
if not whitelist: if not whitelist:
logger.info("Active pair whitelist is empty.") self.log_once("Active pair whitelist is empty.", logger.info)
return trades_created return trades_created
# Remove pairs for currently opened trades from the whitelist # Remove pairs for currently opened trades from the whitelist
for trade in Trade.get_open_trades(): for trade in Trade.get_open_trades():
@ -427,8 +427,8 @@ class FreqtradeBot(LoggingMixin):
logger.debug('Ignoring %s in pair whitelist', trade.pair) logger.debug('Ignoring %s in pair whitelist', trade.pair)
if not whitelist: if not whitelist:
logger.info("No currency pair in active pair whitelist, " self.log_once("No currency pair in active pair whitelist, "
"but checking to exit open trades.") "but checking to exit open trades.", logger.info)
return trades_created return trades_created
if PairLocks.is_global_lock(side='*'): if PairLocks.is_global_lock(side='*'):
# This only checks for total locks (both sides). # This only checks for total locks (both sides).

View File

@ -24,6 +24,7 @@ from freqtrade.enums import (BacktestState, CandleType, ExitCheckTuple, ExitType
TradingMode) TradingMode)
from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
from freqtrade.exchange.exchange import amount_to_precision
from freqtrade.mixins import LoggingMixin from freqtrade.mixins import LoggingMixin
from freqtrade.optimize.backtest_caching import get_strategy_run_id from freqtrade.optimize.backtest_caching import get_strategy_run_id
from freqtrade.optimize.bt_progress import BTProgress from freqtrade.optimize.bt_progress import BTProgress
@ -821,7 +822,15 @@ class Backtesting:
if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount): if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount):
self.order_id_counter += 1 self.order_id_counter += 1
base_currency = self.exchange.get_pair_base_currency(pair) base_currency = self.exchange.get_pair_base_currency(pair)
amount = round((stake_amount / propose_rate) * leverage, 8) amount_p = (stake_amount / propose_rate) * leverage
amount = self.exchange._contracts_to_amount(
pair, amount_to_precision(
self.exchange._amount_to_contracts(pair, amount_p),
self.exchange.get_precision_amount(pair), self.precision_mode)
)
# Backcalculate actual stake amount.
stake_amount = amount * propose_rate / leverage
is_short = (direction == 'short') is_short = (direction == 'short')
# Necessary for Margin trading. Disabled until support is enabled. # Necessary for Margin trading. Disabled until support is enabled.
# interest_rate = self.exchange.get_interest_rate() # interest_rate = self.exchange.get_interest_rate()

View File

@ -307,7 +307,9 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
# Migrates both trades and orders table! # Migrates both trades and orders table!
# if ('orders' not in previous_tables # if ('orders' not in previous_tables
# or not has_column(cols_orders, 'stop_price')): # or not has_column(cols_orders, 'stop_price')):
migrating = False
if not has_column(cols_trades, 'precision_mode'): if not has_column(cols_trades, 'precision_mode'):
migrating = True
logger.info(f"Running database migration for trades - " logger.info(f"Running database migration for trades - "
f"backup: {table_back_name}, {order_table_bak_name}") f"backup: {table_back_name}, {order_table_bak_name}")
migrate_trades_and_orders_table( migrate_trades_and_orders_table(
@ -315,6 +317,7 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
order_table_bak_name, cols_orders) order_table_bak_name, cols_orders)
if not has_column(cols_pairlocks, 'side'): if not has_column(cols_pairlocks, 'side'):
migrating = True
logger.info(f"Running database migration for pairlocks - " logger.info(f"Running database migration for pairlocks - "
f"backup: {pairlock_table_bak_name}") f"backup: {pairlock_table_bak_name}")
@ -329,3 +332,6 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
set_sqlite_to_wal(engine) set_sqlite_to_wal(engine)
fix_old_dry_orders(engine) fix_old_dry_orders(engine)
if migrating:
logger.info("Database migration finished.")

View File

@ -53,7 +53,7 @@ def init_db(db_url: str) -> None:
# https://docs.sqlalchemy.org/en/13/orm/contextual.html#thread-local-scope # https://docs.sqlalchemy.org/en/13/orm/contextual.html#thread-local-scope
# Scoped sessions proxy requests to the appropriate thread-local session. # Scoped sessions proxy requests to the appropriate thread-local session.
# We should use the scoped_session object - not a seperately initialized version # We should use the scoped_session object - not a seperately initialized version
Trade._session = scoped_session(sessionmaker(bind=engine, autoflush=True)) Trade._session = scoped_session(sessionmaker(bind=engine, autoflush=False))
Trade.query = Trade._session.query_property() Trade.query = Trade._session.query_property()
Order.query = Trade._session.query_property() Order.query = Trade._session.query_property()
PairLock.query = Trade._session.query_property() PairLock.query = Trade._session.query_property()

View File

@ -51,6 +51,11 @@ class PrecisionFilter(IPairList):
:param ticker: ticker dict as returned from ccxt.fetch_tickers() :param ticker: ticker dict as returned from ccxt.fetch_tickers()
:return: True if the pair can stay, false if it should be removed :return: True if the pair can stay, false if it should be removed
""" """
if ticker.get('last', None) is None:
self.log_once(f"Removed {ticker['symbol']} from whitelist, because "
"ticker['last'] is empty (Usually no trade in the last 24h).",
logger.info)
return False
stop_price = ticker['last'] * self._stoploss stop_price = ticker['last'] * self._stoploss
# Adjust stop-prices to precision # Adjust stop-prices to precision

View File

@ -30,7 +30,7 @@
"\n", "\n",
"# Initialize empty configuration object\n", "# Initialize empty configuration object\n",
"config = Configuration.from_files([])\n", "config = Configuration.from_files([])\n",
"# Optionally, use existing configuration file\n", "# Optionally (recommended), use existing configuration file\n",
"# config = Configuration.from_files([\"config.json\"])\n", "# config = Configuration.from_files([\"config.json\"])\n",
"\n", "\n",
"# Define some constants\n", "# Define some constants\n",
@ -38,7 +38,7 @@
"# Name of the strategy class\n", "# Name of the strategy class\n",
"config[\"strategy\"] = \"SampleStrategy\"\n", "config[\"strategy\"] = \"SampleStrategy\"\n",
"# Location of the data\n", "# Location of the data\n",
"data_location = Path(config['user_data_dir'], 'data', 'binance')\n", "data_location = config['datadir']\n",
"# Pair to analyze - Only use one pair here\n", "# Pair to analyze - Only use one pair here\n",
"pair = \"BTC/USDT\"" "pair = \"BTC/USDT\""
] ]
@ -365,7 +365,7 @@
"metadata": { "metadata": {
"file_extension": ".py", "file_extension": ".py",
"kernelspec": { "kernelspec": {
"display_name": "Python 3", "display_name": "Python 3.9.7 64-bit ('trade_397')",
"language": "python", "language": "python",
"name": "python3" "name": "python3"
}, },
@ -379,7 +379,7 @@
"name": "python", "name": "python",
"nbconvert_exporter": "python", "nbconvert_exporter": "python",
"pygments_lexer": "ipython3", "pygments_lexer": "ipython3",
"version": "3.8.5" "version": "3.9.7"
}, },
"mimetype": "text/x-python", "mimetype": "text/x-python",
"name": "python", "name": "python",
@ -427,7 +427,12 @@
], ],
"window_display": false "window_display": false
}, },
"version": 3 "version": 3,
"vscode": {
"interpreter": {
"hash": "675f32a300d6d26767470181ad0b11dd4676bcce7ed1dd2ffe2fbc370c95fc7c"
}
}
}, },
"nbformat": 4, "nbformat": 4,
"nbformat_minor": 4 "nbformat_minor": 4

View File

@ -148,7 +148,7 @@ class Wallets:
# Position is not open ... # Position is not open ...
continue continue
size = self._exchange._contracts_to_amount(symbol, position['contracts']) size = self._exchange._contracts_to_amount(symbol, position['contracts'])
collateral = position['collateral'] collateral = position['collateral'] or 0.0
leverage = position['leverage'] leverage = position['leverage']
self._positions[symbol] = PositionWallet( self._positions[symbol] = PositionWallet(
symbol, position=size, symbol, position=size,

View File

@ -1430,6 +1430,27 @@ def test_start_list_data(testdatadir, capsys):
assert "\n| XRP/USDT | 1h | futures |\n" in captured.out assert "\n| XRP/USDT | 1h | futures |\n" in captured.out
assert "\n| XRP/USDT | 1h, 8h | mark |\n" in captured.out assert "\n| XRP/USDT | 1h, 8h | mark |\n" in captured.out
args = [
"list-data",
"--data-format-ohlcv",
"json",
"--pairs", "XRP/ETH",
"--datadir",
str(testdatadir),
"--show-timerange",
]
pargs = get_args(args)
pargs['config'] = None
start_list_data(pargs)
captured = capsys.readouterr()
assert "Found 2 pair / timeframe combinations." in captured.out
assert ("\n| Pair | Timeframe | Type | From | To |\n"
in captured.out)
assert "UNITTEST/BTC" not in captured.out
assert (
"\n| XRP/ETH | 1m | spot | 2019-10-11 00:00:00 | 2019-10-13 11:19:00 |\n"
in captured.out)
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
def test_show_trades(mocker, fee, capsys, caplog): def test_show_trades(mocker, fee, capsys, caplog):

View File

@ -137,6 +137,10 @@ def exchange_futures(request, exchange_conf, class_mocker):
'freqtrade.exchange.binance.Binance.fill_leverage_tiers') 'freqtrade.exchange.binance.Binance.fill_leverage_tiers')
class_mocker.patch('freqtrade.exchange.exchange.Exchange.fetch_trading_fees') class_mocker.patch('freqtrade.exchange.exchange.Exchange.fetch_trading_fees')
class_mocker.patch('freqtrade.exchange.okx.Okx.additional_exchange_init') class_mocker.patch('freqtrade.exchange.okx.Okx.additional_exchange_init')
class_mocker.patch('freqtrade.exchange.exchange.Exchange.load_cached_leverage_tiers',
return_value=None)
class_mocker.patch('freqtrade.exchange.exchange.Exchange.cache_leverage_tiers')
exchange = ExchangeResolver.load_exchange( exchange = ExchangeResolver.load_exchange(
request.param, exchange_conf, validate=True, load_leverage_tiers=True) request.param, exchange_conf, validate=True, load_leverage_tiers=True)

View File

@ -4791,6 +4791,20 @@ def test_load_leverage_tiers(mocker, default_conf, leverage_tiers, exchange_name
) )
@pytest.mark.asyncio
@pytest.mark.parametrize('exchange_name', EXCHANGES)
async def test_get_market_leverage_tiers(mocker, default_conf, exchange_name):
default_conf['exchange']['name'] = exchange_name
await async_ccxt_exception(
mocker,
default_conf,
MagicMock(),
"get_market_leverage_tiers",
"fetch_market_leverage_tiers",
symbol='BTC/USDT:USDT'
)
def test_parse_leverage_tier(mocker, default_conf): def test_parse_leverage_tier(mocker, default_conf):
exchange = get_patched_exchange(mocker, default_conf) exchange = get_patched_exchange(mocker, default_conf)

View File

@ -1,4 +1,5 @@
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from pathlib import Path
from unittest.mock import MagicMock, PropertyMock from unittest.mock import MagicMock, PropertyMock
import pytest import pytest
@ -6,7 +7,7 @@ import pytest
from freqtrade.enums import MarginMode, TradingMode from freqtrade.enums import MarginMode, TradingMode
from freqtrade.enums.candletype import CandleType from freqtrade.enums.candletype import CandleType
from freqtrade.exchange.exchange import timeframe_to_minutes from freqtrade.exchange.exchange import timeframe_to_minutes
from tests.conftest import get_mock_coro, get_patched_exchange from tests.conftest import get_mock_coro, get_patched_exchange, log_has
from tests.exchange.test_exchange import ccxt_exceptionhandlers from tests.exchange.test_exchange import ccxt_exceptionhandlers
@ -267,7 +268,10 @@ def test_additional_exchange_init_okx(default_conf, mocker):
"additional_exchange_init", "fetch_accounts") "additional_exchange_init", "fetch_accounts")
def test_load_leverage_tiers_okx(default_conf, mocker, markets): def test_load_leverage_tiers_okx(default_conf, mocker, markets, tmpdir, caplog, time_machine):
default_conf['datadir'] = Path(tmpdir)
# fd_mock = mocker.patch('freqtrade.exchange.exchange.file_dump_json')
api_mock = MagicMock() api_mock = MagicMock()
type(api_mock).has = PropertyMock(return_value={ type(api_mock).has = PropertyMock(return_value={
'fetchLeverageTiers': False, 'fetchLeverageTiers': False,
@ -455,3 +459,21 @@ def test_load_leverage_tiers_okx(default_conf, mocker, markets):
}, },
], ],
} }
filename = (default_conf['datadir'] /
f"futures/leverage_tiers_{default_conf['stake_currency']}.json")
assert filename.is_file()
logmsg = 'Cached leverage tiers are outdated. Will update.'
assert not log_has(logmsg, caplog)
api_mock.fetch_market_leverage_tiers.reset_mock()
exchange.load_leverage_tiers()
assert not log_has(logmsg, caplog)
api_mock.fetch_market_leverage_tiers.call_count == 0
# 2 day passes ...
time_machine.move_to(datetime.now() + timedelta(days=2))
exchange.load_leverage_tiers()
assert log_has(logmsg, caplog)

View File

@ -559,7 +559,7 @@ def test_backtest__enter_trade_futures(default_conf_usdt, fee, mocker) -> None:
default_conf_usdt['exchange']['pair_whitelist'] = ['.*'] default_conf_usdt['exchange']['pair_whitelist'] = ['.*']
backtesting = Backtesting(default_conf_usdt) backtesting = Backtesting(default_conf_usdt)
backtesting._set_strategy(backtesting.strategylist[0]) backtesting._set_strategy(backtesting.strategylist[0])
pair = 'UNITTEST/USDT:USDT' pair = 'ETH/USDT:USDT'
row = [ row = [
pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=0), pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=0),
0.001, # Open 0.001, # Open

View File

@ -18,6 +18,7 @@ from tests.conftest import patch_exchange
def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) -> None: def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) -> None:
default_conf['use_exit_signal'] = False default_conf['use_exit_signal'] = False
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
mocker.patch('freqtrade.optimize.backtesting.amount_to_precision', lambda x, y, z: round(x, 8))
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf')) mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
patch_exchange(mocker) patch_exchange(mocker)

View File

@ -366,6 +366,9 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
{"method": "PrecisionFilter"}], {"method": "PrecisionFilter"}],
"BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC']), "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC']),
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
{"method": "PrecisionFilter"}],
"USDT", ['ETH/USDT', 'NANO/USDT']),
# PriceFilter and VolumePairList # PriceFilter and VolumePairList
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
{"method": "PriceFilter", "low_price_ratio": 0.03}], {"method": "PriceFilter", "low_price_ratio": 0.03}],

View File

@ -67,6 +67,8 @@ def generate_mock_trade(pair: str, fee: float, is_open: bool,
trade.close(open_rate * (2 - profit_rate if is_short else profit_rate)) trade.close(open_rate * (2 - profit_rate if is_short else profit_rate))
trade.exit_reason = exit_reason trade.exit_reason = exit_reason
Trade.query.session.add(trade)
Trade.commit()
return trade return trade
@ -125,33 +127,33 @@ def test_stoploss_guard(mocker, default_conf, fee, caplog, is_short):
assert not log_has_re(message, caplog) assert not log_has_re(message, caplog)
caplog.clear() caplog.clear()
Trade.query.session.add(generate_mock_trade( generate_mock_trade(
'XRP/BTC', fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value, 'XRP/BTC', fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value,
min_ago_open=200, min_ago_close=30, is_short=is_short, min_ago_open=200, min_ago_close=30, is_short=is_short,
)) )
assert not freqtrade.protections.global_stop() assert not freqtrade.protections.global_stop()
assert not log_has_re(message, caplog) assert not log_has_re(message, caplog)
caplog.clear() caplog.clear()
# This trade does not count, as it's closed too long ago # This trade does not count, as it's closed too long ago
Trade.query.session.add(generate_mock_trade( generate_mock_trade(
'BCH/BTC', fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value, 'BCH/BTC', fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value,
min_ago_open=250, min_ago_close=100, is_short=is_short, min_ago_open=250, min_ago_close=100, is_short=is_short,
)) )
Trade.query.session.add(generate_mock_trade( generate_mock_trade(
'ETH/BTC', fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value, 'ETH/BTC', fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value,
min_ago_open=240, min_ago_close=30, is_short=is_short, min_ago_open=240, min_ago_close=30, is_short=is_short,
)) )
# 3 Trades closed - but the 2nd has been closed too long ago. # 3 Trades closed - but the 2nd has been closed too long ago.
assert not freqtrade.protections.global_stop() assert not freqtrade.protections.global_stop()
assert not log_has_re(message, caplog) assert not log_has_re(message, caplog)
caplog.clear() caplog.clear()
Trade.query.session.add(generate_mock_trade( generate_mock_trade(
'LTC/BTC', fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value, 'LTC/BTC', fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value,
min_ago_open=180, min_ago_close=30, is_short=is_short, min_ago_open=180, min_ago_close=30, is_short=is_short,
)) )
assert freqtrade.protections.global_stop() assert freqtrade.protections.global_stop()
assert log_has_re(message, caplog) assert log_has_re(message, caplog)
@ -186,25 +188,25 @@ def test_stoploss_guard_perpair(mocker, default_conf, fee, caplog, only_per_pair
assert not log_has_re(message, caplog) assert not log_has_re(message, caplog)
caplog.clear() caplog.clear()
Trade.query.session.add(generate_mock_trade( generate_mock_trade(
pair, fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value, pair, fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value,
min_ago_open=200, min_ago_close=30, profit_rate=0.9, is_short=is_short min_ago_open=200, min_ago_close=30, profit_rate=0.9, is_short=is_short
)) )
assert not freqtrade.protections.stop_per_pair(pair) assert not freqtrade.protections.stop_per_pair(pair)
assert not freqtrade.protections.global_stop() assert not freqtrade.protections.global_stop()
assert not log_has_re(message, caplog) assert not log_has_re(message, caplog)
caplog.clear() caplog.clear()
# This trade does not count, as it's closed too long ago # This trade does not count, as it's closed too long ago
Trade.query.session.add(generate_mock_trade( generate_mock_trade(
pair, fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value, pair, fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value,
min_ago_open=250, min_ago_close=100, profit_rate=0.9, is_short=is_short min_ago_open=250, min_ago_close=100, profit_rate=0.9, is_short=is_short
)) )
# Trade does not count for per pair stop as it's the wrong pair. # Trade does not count for per pair stop as it's the wrong pair.
Trade.query.session.add(generate_mock_trade( generate_mock_trade(
'ETH/BTC', fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value, 'ETH/BTC', fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value,
min_ago_open=240, min_ago_close=30, profit_rate=0.9, is_short=is_short min_ago_open=240, min_ago_close=30, profit_rate=0.9, is_short=is_short
)) )
# 3 Trades closed - but the 2nd has been closed too long ago. # 3 Trades closed - but the 2nd has been closed too long ago.
assert not freqtrade.protections.stop_per_pair(pair) assert not freqtrade.protections.stop_per_pair(pair)
assert freqtrade.protections.global_stop() != only_per_pair assert freqtrade.protections.global_stop() != only_per_pair
@ -216,10 +218,10 @@ def test_stoploss_guard_perpair(mocker, default_conf, fee, caplog, only_per_pair
caplog.clear() caplog.clear()
# Trade does not count potentially, as it's in the wrong direction # Trade does not count potentially, as it's in the wrong direction
Trade.query.session.add(generate_mock_trade( generate_mock_trade(
pair, fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value, pair, fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value,
min_ago_open=150, min_ago_close=25, profit_rate=0.9, is_short=not is_short min_ago_open=150, min_ago_close=25, profit_rate=0.9, is_short=not is_short
)) )
freqtrade.protections.stop_per_pair(pair) freqtrade.protections.stop_per_pair(pair)
assert freqtrade.protections.global_stop() != only_per_pair assert freqtrade.protections.global_stop() != only_per_pair
assert PairLocks.is_pair_locked(pair, side=check_side) != (only_per_side and only_per_pair) assert PairLocks.is_pair_locked(pair, side=check_side) != (only_per_side and only_per_pair)
@ -231,10 +233,10 @@ def test_stoploss_guard_perpair(mocker, default_conf, fee, caplog, only_per_pair
caplog.clear() caplog.clear()
# 2nd Trade that counts with correct pair # 2nd Trade that counts with correct pair
Trade.query.session.add(generate_mock_trade( generate_mock_trade(
pair, fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value, pair, fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value,
min_ago_open=180, min_ago_close=30, profit_rate=0.9, is_short=is_short min_ago_open=180, min_ago_close=30, profit_rate=0.9, is_short=is_short
)) )
freqtrade.protections.stop_per_pair(pair) freqtrade.protections.stop_per_pair(pair)
assert freqtrade.protections.global_stop() != only_per_pair assert freqtrade.protections.global_stop() != only_per_pair
@ -259,20 +261,20 @@ def test_CooldownPeriod(mocker, default_conf, fee, caplog):
assert not log_has_re(message, caplog) assert not log_has_re(message, caplog)
caplog.clear() caplog.clear()
Trade.query.session.add(generate_mock_trade( generate_mock_trade(
'XRP/BTC', fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value, 'XRP/BTC', fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value,
min_ago_open=200, min_ago_close=30, min_ago_open=200, min_ago_close=30,
)) )
assert not freqtrade.protections.global_stop() assert not freqtrade.protections.global_stop()
assert freqtrade.protections.stop_per_pair('XRP/BTC') assert freqtrade.protections.stop_per_pair('XRP/BTC')
assert PairLocks.is_pair_locked('XRP/BTC') assert PairLocks.is_pair_locked('XRP/BTC')
assert not PairLocks.is_global_lock() assert not PairLocks.is_global_lock()
Trade.query.session.add(generate_mock_trade( generate_mock_trade(
'ETH/BTC', fee.return_value, False, exit_reason=ExitType.ROI.value, 'ETH/BTC', fee.return_value, False, exit_reason=ExitType.ROI.value,
min_ago_open=205, min_ago_close=35, min_ago_open=205, min_ago_close=35,
)) )
assert not freqtrade.protections.global_stop() assert not freqtrade.protections.global_stop()
assert not PairLocks.is_pair_locked('ETH/BTC') assert not PairLocks.is_pair_locked('ETH/BTC')
@ -300,10 +302,10 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog, only_per_side):
assert not log_has_re(message, caplog) assert not log_has_re(message, caplog)
caplog.clear() caplog.clear()
Trade.query.session.add(generate_mock_trade( generate_mock_trade(
'XRP/BTC', fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value, 'XRP/BTC', fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value,
min_ago_open=800, min_ago_close=450, profit_rate=0.9, min_ago_open=800, min_ago_close=450, profit_rate=0.9,
)) )
Trade.commit() Trade.commit()
# Not locked with 1 trade # Not locked with 1 trade
@ -312,10 +314,10 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog, only_per_side):
assert not PairLocks.is_pair_locked('XRP/BTC') assert not PairLocks.is_pair_locked('XRP/BTC')
assert not PairLocks.is_global_lock() assert not PairLocks.is_global_lock()
Trade.query.session.add(generate_mock_trade( generate_mock_trade(
'XRP/BTC', fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value, 'XRP/BTC', fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value,
min_ago_open=200, min_ago_close=120, profit_rate=0.9, min_ago_open=200, min_ago_close=120, profit_rate=0.9,
)) )
Trade.commit() Trade.commit()
# Not locked with 1 trade (first trade is outside of lookback_period) # Not locked with 1 trade (first trade is outside of lookback_period)
@ -325,19 +327,19 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog, only_per_side):
assert not PairLocks.is_global_lock() assert not PairLocks.is_global_lock()
# Add positive trade # Add positive trade
Trade.query.session.add(generate_mock_trade( generate_mock_trade(
'XRP/BTC', fee.return_value, False, exit_reason=ExitType.ROI.value, 'XRP/BTC', fee.return_value, False, exit_reason=ExitType.ROI.value,
min_ago_open=20, min_ago_close=10, profit_rate=1.15, is_short=True min_ago_open=20, min_ago_close=10, profit_rate=1.15, is_short=True
)) )
Trade.commit() Trade.commit()
assert freqtrade.protections.stop_per_pair('XRP/BTC') != only_per_side assert freqtrade.protections.stop_per_pair('XRP/BTC') != only_per_side
assert not PairLocks.is_pair_locked('XRP/BTC', side='*') assert not PairLocks.is_pair_locked('XRP/BTC', side='*')
assert PairLocks.is_pair_locked('XRP/BTC', side='long') == only_per_side assert PairLocks.is_pair_locked('XRP/BTC', side='long') == only_per_side
Trade.query.session.add(generate_mock_trade( generate_mock_trade(
'XRP/BTC', fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value, 'XRP/BTC', fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value,
min_ago_open=110, min_ago_close=21, profit_rate=0.8, min_ago_open=110, min_ago_close=21, profit_rate=0.8,
)) )
Trade.commit() Trade.commit()
# Locks due to 2nd trade # Locks due to 2nd trade
@ -365,36 +367,38 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog):
assert not freqtrade.protections.stop_per_pair('XRP/BTC') assert not freqtrade.protections.stop_per_pair('XRP/BTC')
caplog.clear() caplog.clear()
Trade.query.session.add(generate_mock_trade( generate_mock_trade(
'XRP/BTC', fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value, 'XRP/BTC', fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value,
min_ago_open=1000, min_ago_close=900, profit_rate=1.1, min_ago_open=1000, min_ago_close=900, profit_rate=1.1,
)) )
Trade.query.session.add(generate_mock_trade( generate_mock_trade(
'ETH/BTC', fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value, 'ETH/BTC', fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value,
min_ago_open=1000, min_ago_close=900, profit_rate=1.1, min_ago_open=1000, min_ago_close=900, profit_rate=1.1,
)) )
Trade.query.session.add(generate_mock_trade( generate_mock_trade(
'NEO/BTC', fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value, 'NEO/BTC', fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value,
min_ago_open=1000, min_ago_close=900, profit_rate=1.1, min_ago_open=1000, min_ago_close=900, profit_rate=1.1,
)) )
Trade.commit()
# No losing trade yet ... so max_drawdown will raise exception # No losing trade yet ... so max_drawdown will raise exception
assert not freqtrade.protections.global_stop() assert not freqtrade.protections.global_stop()
assert not freqtrade.protections.stop_per_pair('XRP/BTC') assert not freqtrade.protections.stop_per_pair('XRP/BTC')
Trade.query.session.add(generate_mock_trade( generate_mock_trade(
'XRP/BTC', fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value, 'XRP/BTC', fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value,
min_ago_open=500, min_ago_close=400, profit_rate=0.9, min_ago_open=500, min_ago_close=400, profit_rate=0.9,
)) )
# Not locked with one trade # Not locked with one trade
assert not freqtrade.protections.global_stop() assert not freqtrade.protections.global_stop()
assert not freqtrade.protections.stop_per_pair('XRP/BTC') assert not freqtrade.protections.stop_per_pair('XRP/BTC')
assert not PairLocks.is_pair_locked('XRP/BTC') assert not PairLocks.is_pair_locked('XRP/BTC')
assert not PairLocks.is_global_lock() assert not PairLocks.is_global_lock()
Trade.query.session.add(generate_mock_trade( generate_mock_trade(
'XRP/BTC', fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value, 'XRP/BTC', fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value,
min_ago_open=1200, min_ago_close=1100, profit_rate=0.5, min_ago_open=1200, min_ago_close=1100, profit_rate=0.5,
)) )
Trade.commit()
# Not locked with 1 trade (2nd trade is outside of lookback_period) # Not locked with 1 trade (2nd trade is outside of lookback_period)
assert not freqtrade.protections.global_stop() assert not freqtrade.protections.global_stop()
@ -404,20 +408,22 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog):
assert not log_has_re(message, caplog) assert not log_has_re(message, caplog)
# Winning trade ... (should not lock, does not change drawdown!) # Winning trade ... (should not lock, does not change drawdown!)
Trade.query.session.add(generate_mock_trade( generate_mock_trade(
'XRP/BTC', fee.return_value, False, exit_reason=ExitType.ROI.value, 'XRP/BTC', fee.return_value, False, exit_reason=ExitType.ROI.value,
min_ago_open=320, min_ago_close=410, profit_rate=1.5, min_ago_open=320, min_ago_close=410, profit_rate=1.5,
)) )
Trade.commit()
assert not freqtrade.protections.global_stop() assert not freqtrade.protections.global_stop()
assert not PairLocks.is_global_lock() assert not PairLocks.is_global_lock()
caplog.clear() caplog.clear()
# Add additional negative trade, causing a loss of > 15% # Add additional negative trade, causing a loss of > 15%
Trade.query.session.add(generate_mock_trade( generate_mock_trade(
'XRP/BTC', fee.return_value, False, exit_reason=ExitType.ROI.value, 'XRP/BTC', fee.return_value, False, exit_reason=ExitType.ROI.value,
min_ago_open=20, min_ago_close=10, profit_rate=0.8, min_ago_open=20, min_ago_close=10, profit_rate=0.8,
)) )
Trade.commit()
assert not freqtrade.protections.stop_per_pair('XRP/BTC') assert not freqtrade.protections.stop_per_pair('XRP/BTC')
# local lock not supported # local lock not supported
assert not PairLocks.is_pair_locked('XRP/BTC') assert not PairLocks.is_pair_locked('XRP/BTC')

View File

@ -677,6 +677,7 @@ def test_process_trade_no_whitelist_pair(default_conf_usdt, ticker_usdt, limit_b
open_rate=0.001, open_rate=0.001,
exchange='binance', exchange='binance',
)) ))
Trade.commit()
assert pair not in freqtrade.active_pair_whitelist assert pair not in freqtrade.active_pair_whitelist
freqtrade.process() freqtrade.process()
@ -2414,6 +2415,7 @@ def test_manage_open_orders_entry_usercustom(
open_trade.orders[0].side = 'sell' if is_short else 'buy' open_trade.orders[0].side = 'sell' if is_short else 'buy'
open_trade.orders[0].ft_order_side = 'sell' if is_short else 'buy' open_trade.orders[0].ft_order_side = 'sell' if is_short else 'buy'
Trade.query.session.add(open_trade) Trade.query.session.add(open_trade)
Trade.commit()
# Ensure default is to return empty (so not mocked yet) # Ensure default is to return empty (so not mocked yet)
freqtrade.manage_open_orders() freqtrade.manage_open_orders()
@ -2472,6 +2474,7 @@ def test_manage_open_orders_entry(
open_trade.is_short = is_short open_trade.is_short = is_short
Trade.query.session.add(open_trade) Trade.query.session.add(open_trade)
Trade.commit()
freqtrade.strategy.check_entry_timeout = MagicMock(return_value=False) freqtrade.strategy.check_entry_timeout = MagicMock(return_value=False)
freqtrade.strategy.adjust_entry_price = MagicMock(return_value=1234) freqtrade.strategy.adjust_entry_price = MagicMock(return_value=1234)
@ -2509,6 +2512,7 @@ def test_adjust_entry_cancel(
open_trade.is_short = is_short open_trade.is_short = is_short
Trade.query.session.add(open_trade) Trade.query.session.add(open_trade)
Trade.commit()
# Timeout to not interfere # Timeout to not interfere
freqtrade.strategy.ft_check_timed_out = MagicMock(return_value=False) freqtrade.strategy.ft_check_timed_out = MagicMock(return_value=False)
@ -2549,6 +2553,7 @@ def test_adjust_entry_maintain_replace(
open_trade.is_short = is_short open_trade.is_short = is_short
Trade.query.session.add(open_trade) Trade.query.session.add(open_trade)
Trade.commit()
# Timeout to not interfere # Timeout to not interfere
freqtrade.strategy.ft_check_timed_out = MagicMock(return_value=False) freqtrade.strategy.ft_check_timed_out = MagicMock(return_value=False)
@ -2601,6 +2606,7 @@ def test_check_handle_cancelled_buy(
open_trade.orders = [] open_trade.orders = []
open_trade.is_short = is_short open_trade.is_short = is_short
Trade.query.session.add(open_trade) Trade.query.session.add(open_trade)
Trade.commit()
# check it does cancel buy orders over the time limit # check it does cancel buy orders over the time limit
freqtrade.manage_open_orders() freqtrade.manage_open_orders()
@ -2631,6 +2637,7 @@ def test_manage_open_orders_buy_exception(
open_trade.is_short = is_short open_trade.is_short = is_short
Trade.query.session.add(open_trade) Trade.query.session.add(open_trade)
Trade.commit()
# check it does cancel buy orders over the time limit # check it does cancel buy orders over the time limit
freqtrade.manage_open_orders() freqtrade.manage_open_orders()
@ -2672,6 +2679,7 @@ def test_manage_open_orders_exit_usercustom(
open_trade_usdt.is_open = False open_trade_usdt.is_open = False
Trade.query.session.add(open_trade_usdt) Trade.query.session.add(open_trade_usdt)
Trade.commit()
# Ensure default is false # Ensure default is false
freqtrade.manage_open_orders() freqtrade.manage_open_orders()
assert cancel_order_mock.call_count == 0 assert cancel_order_mock.call_count == 0
@ -2754,6 +2762,7 @@ def test_manage_open_orders_exit(
open_trade_usdt.is_short = is_short open_trade_usdt.is_short = is_short
Trade.query.session.add(open_trade_usdt) Trade.query.session.add(open_trade_usdt)
Trade.commit()
freqtrade.strategy.check_exit_timeout = MagicMock(return_value=False) freqtrade.strategy.check_exit_timeout = MagicMock(return_value=False)
freqtrade.strategy.check_entry_timeout = MagicMock(return_value=False) freqtrade.strategy.check_entry_timeout = MagicMock(return_value=False)
@ -2794,6 +2803,7 @@ def test_check_handle_cancelled_exit(
open_trade_usdt.is_short = is_short open_trade_usdt.is_short = is_short
Trade.query.session.add(open_trade_usdt) Trade.query.session.add(open_trade_usdt)
Trade.commit()
# check it does cancel sell orders over the time limit # check it does cancel sell orders over the time limit
freqtrade.manage_open_orders() freqtrade.manage_open_orders()
@ -2830,6 +2840,7 @@ def test_manage_open_orders_partial(
freqtrade = FreqtradeBot(default_conf_usdt) freqtrade = FreqtradeBot(default_conf_usdt)
prior_stake = open_trade.stake_amount prior_stake = open_trade.stake_amount
Trade.query.session.add(open_trade) Trade.query.session.add(open_trade)
Trade.commit()
# check it does cancel buy orders over the time limit # check it does cancel buy orders over the time limit
# note this is for a partially-complete buy order # note this is for a partially-complete buy order
@ -2874,6 +2885,7 @@ def test_manage_open_orders_partial_fee(
open_trade.fee_open = fee() open_trade.fee_open = fee()
open_trade.fee_close = fee() open_trade.fee_close = fee()
Trade.query.session.add(open_trade) Trade.query.session.add(open_trade)
Trade.commit()
# cancelling a half-filled order should update the amount to the bought amount # cancelling a half-filled order should update the amount to the bought amount
# and apply fees if necessary. # and apply fees if necessary.
freqtrade.manage_open_orders() freqtrade.manage_open_orders()
@ -2923,6 +2935,7 @@ def test_manage_open_orders_partial_except(
open_trade.fee_open = fee() open_trade.fee_open = fee()
open_trade.fee_close = fee() open_trade.fee_close = fee()
Trade.query.session.add(open_trade) Trade.query.session.add(open_trade)
Trade.commit()
# cancelling a half-filled order should update the amount to the bought amount # cancelling a half-filled order should update the amount to the bought amount
# and apply fees if necessary. # and apply fees if necessary.
freqtrade.manage_open_orders() freqtrade.manage_open_orders()
@ -2961,6 +2974,7 @@ def test_manage_open_orders_exception(default_conf_usdt, ticker_usdt, open_trade
freqtrade = FreqtradeBot(default_conf_usdt) freqtrade = FreqtradeBot(default_conf_usdt)
Trade.query.session.add(open_trade_usdt) Trade.query.session.add(open_trade_usdt)
Trade.commit()
caplog.clear() caplog.clear()
freqtrade.manage_open_orders() freqtrade.manage_open_orders()

View File

@ -1387,6 +1387,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
assert log_has("trying trades_bak2", caplog) assert log_has("trying trades_bak2", caplog)
assert log_has("Running database migration for trades - backup: trades_bak2, orders_bak0", assert log_has("Running database migration for trades - backup: trades_bak2, orders_bak0",
caplog) caplog)
assert log_has("Database migration finished.", caplog)
assert pytest.approx(trade.open_trade_value) == trade._calc_open_trade_value( assert pytest.approx(trade.open_trade_value) == trade._calc_open_trade_value(
trade.amount, trade.open_rate) trade.amount, trade.open_rate)
assert trade.close_profit_abs is None assert trade.close_profit_abs is None
@ -1885,6 +1886,7 @@ def test_stoploss_reinitialization(default_conf, fee):
assert trade.initial_stop_loss == 0.95 assert trade.initial_stop_loss == 0.95
assert trade.initial_stop_loss_pct == -0.05 assert trade.initial_stop_loss_pct == -0.05
Trade.query.session.add(trade) Trade.query.session.add(trade)
Trade.commit()
# Lower stoploss # Lower stoploss
Trade.stoploss_reinitialization(0.06) Trade.stoploss_reinitialization(0.06)
@ -1946,6 +1948,7 @@ def test_stoploss_reinitialization_leverage(default_conf, fee):
assert trade.initial_stop_loss == 0.98 assert trade.initial_stop_loss == 0.98
assert trade.initial_stop_loss_pct == -0.1 assert trade.initial_stop_loss_pct == -0.1
Trade.query.session.add(trade) Trade.query.session.add(trade)
Trade.commit()
# Lower stoploss # Lower stoploss
Trade.stoploss_reinitialization(0.15) Trade.stoploss_reinitialization(0.15)
@ -2007,6 +2010,7 @@ def test_stoploss_reinitialization_short(default_conf, fee):
assert trade.initial_stop_loss == 1.02 assert trade.initial_stop_loss == 1.02
assert trade.initial_stop_loss_pct == -0.1 assert trade.initial_stop_loss_pct == -0.1
Trade.query.session.add(trade) Trade.query.session.add(trade)
Trade.commit()
# Lower stoploss # Lower stoploss
Trade.stoploss_reinitialization(-0.15) Trade.stoploss_reinitialization(-0.15)
trades = Trade.get_open_trades() trades = Trade.get_open_trades()