Merge branch 'develop' into patch-10

This commit is contained in:
மனோஜ்குமார் பழனிச்சாமி 2022-05-17 08:07:13 +05:30 committed by GitHub
commit 7cd0f8a7b1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 352 additions and 127 deletions

View File

@ -19,7 +19,7 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
os: [ ubuntu-18.04, ubuntu-20.04 ] os: [ ubuntu-18.04, ubuntu-20.04, ubuntu-22.04 ]
python-version: ["3.8", "3.9", "3.10"] python-version: ["3.8", "3.9", "3.10"]
steps: steps:
@ -70,7 +70,7 @@ jobs:
if: matrix.python-version == '3.9' if: matrix.python-version == '3.9'
- name: Coveralls - name: Coveralls
if: (runner.os == 'Linux' && matrix.python-version == '3.8') if: (runner.os == 'Linux' && matrix.python-version == '3.9')
env: env:
# Coveralls token. Not used as secret due to github not providing secrets to forked repositories # Coveralls token. Not used as secret due to github not providing secrets to forked repositories
COVERALLS_REPO_TOKEN: 6D1m0xupS3FgutfuGao8keFf9Hc0FpIXu COVERALLS_REPO_TOKEN: 6D1m0xupS3FgutfuGao8keFf9Hc0FpIXu
@ -157,24 +157,9 @@ jobs:
pip install -e . pip install -e .
- name: Tests - name: Tests
if: (runner.os != 'Linux' || matrix.python-version != '3.8')
run: | run: |
pytest --random-order pytest --random-order
- name: Tests (with cov)
if: (runner.os == 'Linux' && matrix.python-version == '3.8')
run: |
pytest --random-order --cov=freqtrade --cov-config=.coveragerc
- name: Coveralls
if: (runner.os == 'Linux' && matrix.python-version == '3.8')
env:
# Coveralls token. Not used as secret due to github not providing secrets to forked repositories
COVERALLS_REPO_TOKEN: 6D1m0xupS3FgutfuGao8keFf9Hc0FpIXu
run: |
# Allow failure for coveralls
coveralls -v || true
- name: Backtesting - name: Backtesting
run: | run: |
cp config_examples/config_bittrex.example.json config.json cp config_examples/config_bittrex.example.json config.json
@ -273,7 +258,7 @@ jobs:
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v3 uses: actions/setup-python@v3
with: with:
python-version: 3.9 python-version: "3.10"
- name: pre-commit dependencies - name: pre-commit dependencies
run: | run: |
@ -292,7 +277,7 @@ jobs:
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v3 uses: actions/setup-python@v3
with: with:
python-version: 3.9 python-version: "3.10"
- name: Documentation build - name: Documentation build
run: | run: |
@ -358,7 +343,7 @@ jobs:
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v3 uses: actions/setup-python@v3
with: with:
python-version: 3.8 python-version: "3.9"
- name: Extract branch name - name: Extract branch name
shell: bash shell: bash

View File

@ -1,4 +1,4 @@
FROM python:3.9.9-slim-bullseye as base FROM python:3.10.4-slim-bullseye as base
# Setup env # Setup env
ENV LANG C.UTF-8 ENV LANG C.UTF-8

View File

@ -1,4 +1,4 @@
FROM python:3.9.9-slim-bullseye as base FROM python:3.9.12-slim-bullseye as base
# Setup env # Setup env
ENV LANG C.UTF-8 ENV LANG C.UTF-8

View File

@ -160,17 +160,17 @@ This filter allows freqtrade to ignore pairs until they have been listed for at
Offsets an incoming pairlist by a given `offset` value. Offsets an incoming pairlist by a given `offset` value.
As an example it can be used in conjunction with `VolumeFilter` to remove the top X volume pairs. Or to split As an example it can be used in conjunction with `VolumeFilter` to remove the top X volume pairs. Or to split a larger pairlist on two bot instances.
a larger pairlist on two bot instances.
Example to remove the first 10 pairs from the pairlist: Example to remove the first 10 pairs from the pairlist, and takes the next 20 (taking items 10-30 of the initial list):
```json ```json
"pairlists": [ "pairlists": [
// ... // ...
{ {
"method": "OffsetFilter", "method": "OffsetFilter",
"offset": 10 "offset": 10,
"number_assets": 20
} }
], ],
``` ```

View File

@ -1,5 +1,5 @@
mkdocs==1.3.0 mkdocs==1.3.0
mkdocs-material==8.2.14 mkdocs-material==8.2.15
mdx_truly_sane_lists==1.2 mdx_truly_sane_lists==1.2
pymdown-extensions==9.4 pymdown-extensions==9.4
jinja2==3.1.2 jinja2==3.1.2

View File

@ -79,6 +79,12 @@ def start_download_data(args: Dict[str, Any]) -> None:
data_format_trades=config['dataformat_trades'], data_format_trades=config['dataformat_trades'],
) )
else: else:
if not exchange._ft_has.get('ohlcv_has_history', True):
raise OperationalException(
f"Historic klines not available for {exchange.name}. "
"Please use `--dl-trades` instead for this exchange "
"(will unfortunately take a long time)."
)
pairs_not_available = refresh_backtest_ohlcv_data( pairs_not_available = refresh_backtest_ohlcv_data(
exchange, pairs=expanded_pairs, timeframes=config['timeframes'], exchange, pairs=expanded_pairs, timeframes=config['timeframes'],
datadir=config['datadir'], timerange=timerange, datadir=config['datadir'], timerange=timerange,

View File

@ -64,6 +64,7 @@ class Exchange:
"time_in_force_parameter": "timeInForce", "time_in_force_parameter": "timeInForce",
"ohlcv_params": {}, "ohlcv_params": {},
"ohlcv_candle_limit": 500, "ohlcv_candle_limit": 500,
"ohlcv_has_history": True, # Some exchanges (Kraken) don't provide history via ohlcv
"ohlcv_partial_candle": True, "ohlcv_partial_candle": True,
"ohlcv_require_since": False, "ohlcv_require_since": False,
# Check https://github.com/ccxt/ccxt/issues/10767 for removal of ohlcv_volume_currency # Check https://github.com/ccxt/ccxt/issues/10767 for removal of ohlcv_volume_currency
@ -308,12 +309,15 @@ class Exchange:
if self.log_responses: if self.log_responses:
logger.info(f"API {endpoint}: {response}") logger.info(f"API {endpoint}: {response}")
def ohlcv_candle_limit(self, timeframe: str) -> int: def ohlcv_candle_limit(
self, timeframe: str, candle_type: CandleType, since_ms: Optional[int] = None) -> int:
""" """
Exchange ohlcv candle limit Exchange ohlcv candle limit
Uses ohlcv_candle_limit_per_timeframe if the exchange has different limits Uses ohlcv_candle_limit_per_timeframe if the exchange has different limits
per timeframe (e.g. bittrex), otherwise falls back to ohlcv_candle_limit per timeframe (e.g. bittrex), otherwise falls back to ohlcv_candle_limit
:param timeframe: Timeframe to check :param timeframe: Timeframe to check
:param candle_type: Candle-type
:param since_ms: Starting timestamp
:return: Candle limit as integer :return: Candle limit as integer
""" """
return int(self._ft_has.get('ohlcv_candle_limit_per_timeframe', {}).get( return int(self._ft_has.get('ohlcv_candle_limit_per_timeframe', {}).get(
@ -615,19 +619,28 @@ class Exchange:
Checks if required startup_candles is more than ohlcv_candle_limit(). Checks if required startup_candles is more than ohlcv_candle_limit().
Requires a grace-period of 5 candles - so a startup-period up to 494 is allowed by default. Requires a grace-period of 5 candles - so a startup-period up to 494 is allowed by default.
""" """
candle_limit = self.ohlcv_candle_limit(timeframe)
candle_limit = self.ohlcv_candle_limit(
timeframe, self._config['candle_type_def'],
int(date_minus_candles(timeframe, startup_candles).timestamp() * 1000)
if timeframe else None)
# Require one more candle - to account for the still open candle. # Require one more candle - to account for the still open candle.
candle_count = startup_candles + 1 candle_count = startup_candles + 1
# Allow 5 calls to the exchange per pair # Allow 5 calls to the exchange per pair
required_candle_call_count = int( required_candle_call_count = int(
(candle_count / candle_limit) + (0 if candle_count % candle_limit == 0 else 1)) (candle_count / candle_limit) + (0 if candle_count % candle_limit == 0 else 1))
if self._ft_has['ohlcv_has_history']:
if required_candle_call_count > 5: if required_candle_call_count > 5:
# Only allow 5 calls per pair to somewhat limit the impact # Only allow 5 calls per pair to somewhat limit the impact
raise OperationalException( raise OperationalException(
f"This strategy requires {startup_candles} candles to start, which is more than 5x " f"This strategy requires {startup_candles} candles to start, "
"which is more than 5x "
f"the amount of candles {self.name} provides for {timeframe}.")
elif required_candle_call_count > 1:
raise OperationalException(
f"This strategy requires {startup_candles} candles to start, which is more than "
f"the amount of candles {self.name} provides for {timeframe}.") f"the amount of candles {self.name} provides for {timeframe}.")
if required_candle_call_count > 1: if required_candle_call_count > 1:
logger.warning(f"Using {required_candle_call_count} calls to get OHLCV. " logger.warning(f"Using {required_candle_call_count} calls to get OHLCV. "
f"This can result in slower operations for the bot. Please check " f"This can result in slower operations for the bot. Please check "
@ -1444,6 +1457,23 @@ class Exchange:
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) from e raise OperationalException(e) from e
def _get_price_side(self, side: str, is_short: bool, conf_strategy: Dict) -> str:
price_side = conf_strategy['price_side']
if price_side in ('same', 'other'):
price_map = {
('entry', 'long', 'same'): 'bid',
('entry', 'long', 'other'): 'ask',
('entry', 'short', 'same'): 'ask',
('entry', 'short', 'other'): 'bid',
('exit', 'long', 'same'): 'ask',
('exit', 'long', 'other'): 'bid',
('exit', 'short', 'same'): 'bid',
('exit', 'short', 'other'): 'ask',
}
price_side = price_map[(side, 'short' if is_short else 'long', price_side)]
return price_side
def get_rate(self, pair: str, refresh: bool, def get_rate(self, pair: str, refresh: bool,
side: EntryExit, is_short: bool) -> float: side: EntryExit, is_short: bool) -> float:
""" """
@ -1470,20 +1500,7 @@ class Exchange:
conf_strategy = self._config.get(strat_name, {}) conf_strategy = self._config.get(strat_name, {})
price_side = conf_strategy['price_side'] price_side = self._get_price_side(side, is_short, conf_strategy)
if price_side in ('same', 'other'):
price_map = {
('entry', 'long', 'same'): 'bid',
('entry', 'long', 'other'): 'ask',
('entry', 'short', 'same'): 'ask',
('entry', 'short', 'other'): 'bid',
('exit', 'long', 'same'): 'ask',
('exit', 'long', 'other'): 'bid',
('exit', 'short', 'same'): 'bid',
('exit', 'short', 'other'): 'ask',
}
price_side = price_map[(side, 'short' if is_short else 'long', price_side)]
price_side_word = price_side.capitalize() price_side_word = price_side.capitalize()
@ -1703,7 +1720,8 @@ class Exchange:
:param candle_type: Any of the enum CandleType (must match trading mode!) :param candle_type: Any of the enum CandleType (must match trading mode!)
""" """
one_call = timeframe_to_msecs(timeframe) * self.ohlcv_candle_limit(timeframe) one_call = timeframe_to_msecs(timeframe) * self.ohlcv_candle_limit(
timeframe, candle_type, since_ms)
logger.debug( logger.debug(
"one_call: %s msecs (%s)", "one_call: %s msecs (%s)",
one_call, one_call,
@ -1739,7 +1757,8 @@ class Exchange:
if (not since_ms if (not since_ms
and (self._ft_has["ohlcv_require_since"] or self.required_candle_call_count > 1)): and (self._ft_has["ohlcv_require_since"] or self.required_candle_call_count > 1)):
# Multiple calls for one pair - to get more history # Multiple calls for one pair - to get more history
one_call = timeframe_to_msecs(timeframe) * self.ohlcv_candle_limit(timeframe) one_call = timeframe_to_msecs(timeframe) * self.ohlcv_candle_limit(
timeframe, candle_type, since_ms)
move_to = one_call * self.required_candle_call_count move_to = one_call * self.required_candle_call_count
now = timeframe_to_next_date(timeframe) now = timeframe_to_next_date(timeframe)
since_ms = int((now - timedelta(seconds=move_to // 1000)).timestamp() * 1000) since_ms = int((now - timedelta(seconds=move_to // 1000)).timestamp() * 1000)
@ -1857,7 +1876,9 @@ class Exchange:
pair, timeframe, since_ms, s pair, timeframe, since_ms, s
) )
params = deepcopy(self._ft_has.get('ohlcv_params', {})) params = deepcopy(self._ft_has.get('ohlcv_params', {}))
candle_limit = self.ohlcv_candle_limit(timeframe) candle_limit = self.ohlcv_candle_limit(
timeframe, candle_type=candle_type, since_ms=since_ms)
if candle_type != CandleType.SPOT: if candle_type != CandleType.SPOT:
params.update({'price': candle_type}) params.update({'price': candle_type})
if candle_type != CandleType.FUNDING_RATE: if candle_type != CandleType.FUNDING_RATE:
@ -2674,9 +2695,10 @@ def timeframe_to_msecs(timeframe: str) -> int:
def timeframe_to_prev_date(timeframe: str, date: datetime = None) -> datetime: def timeframe_to_prev_date(timeframe: str, date: datetime = None) -> datetime:
""" """
Use Timeframe and determine last possible candle. Use Timeframe and determine the candle start date for this date.
Does not round when given a candle start date.
:param timeframe: timeframe in string format (e.g. "5m") :param timeframe: timeframe in string format (e.g. "5m")
:param date: date to use. Defaults to utcnow() :param date: date to use. Defaults to now(utc)
:returns: date of previous candle (with utc timezone) :returns: date of previous candle (with utc timezone)
""" """
if not date: if not date:
@ -2691,7 +2713,7 @@ def timeframe_to_next_date(timeframe: str, date: datetime = None) -> datetime:
""" """
Use Timeframe and determine next candle. Use Timeframe and determine next candle.
:param timeframe: timeframe in string format (e.g. "5m") :param timeframe: timeframe in string format (e.g. "5m")
:param date: date to use. Defaults to utcnow() :param date: date to use. Defaults to now(utc)
:returns: date of next candle (with utc timezone) :returns: date of next candle (with utc timezone)
""" """
if not date: if not date:
@ -2701,6 +2723,23 @@ def timeframe_to_next_date(timeframe: str, date: datetime = None) -> datetime:
return datetime.fromtimestamp(new_timestamp, tz=timezone.utc) return datetime.fromtimestamp(new_timestamp, tz=timezone.utc)
def date_minus_candles(
timeframe: str, candle_count: int, date: Optional[datetime] = None) -> datetime:
"""
subtract X candles from a date.
:param timeframe: timeframe in string format (e.g. "5m")
:param candle_count: Amount of candles to subtract.
:param date: date to use. Defaults to now(utc)
"""
if not date:
date = datetime.now(timezone.utc)
tf_min = timeframe_to_minutes(timeframe)
new_date = timeframe_to_prev_date(timeframe, date) - timedelta(minutes=tf_min * candle_count)
return new_date
def market_is_active(market: Dict) -> bool: def market_is_active(market: Dict) -> bool:
""" """
Return True if the market is active. Return True if the market is active.

View File

@ -23,6 +23,7 @@ class Kraken(Exchange):
_ft_has: Dict = { _ft_has: Dict = {
"stoploss_on_exchange": True, "stoploss_on_exchange": True,
"ohlcv_candle_limit": 720, "ohlcv_candle_limit": 720,
"ohlcv_has_history": False,
"trades_pagination": "id", "trades_pagination": "id",
"trades_pagination_arg": "since", "trades_pagination_arg": "since",
"mark_ohlcv_timeframe": "4h", "mark_ohlcv_timeframe": "4h",

View File

@ -1,13 +1,15 @@
import logging import logging
from typing import Dict, List, Tuple from typing import Dict, List, Optional, Tuple
import ccxt import ccxt
from freqtrade.constants import BuySell from freqtrade.constants import BuySell
from freqtrade.enums import MarginMode, TradingMode from freqtrade.enums import MarginMode, TradingMode
from freqtrade.enums.candletype import CandleType
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.exchange.common import retrier from freqtrade.exchange.common import retrier
from freqtrade.exchange.exchange import date_minus_candles
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -20,7 +22,7 @@ class Okx(Exchange):
""" """
_ft_has: Dict = { _ft_has: Dict = {
"ohlcv_candle_limit": 100, "ohlcv_candle_limit": 100, # Warning, special case with data prior to X months
"mark_ohlcv_timeframe": "4h", "mark_ohlcv_timeframe": "4h",
"funding_fee_timeframe": "8h", "funding_fee_timeframe": "8h",
} }
@ -37,6 +39,27 @@ class Okx(Exchange):
net_only = True net_only = True
def ohlcv_candle_limit(
self, timeframe: str, candle_type: CandleType, since_ms: Optional[int] = None) -> int:
"""
Exchange ohlcv candle limit
OKX has the following behaviour:
* 300 candles for uptodate data
* 100 candles for historic data
* 100 candles for additional candles (not futures or spot).
:param timeframe: Timeframe to check
:param candle_type: Candle-type
:param since_ms: Starting timestamp
:return: Candle limit as integer
"""
if (
candle_type in (CandleType.FUTURES, CandleType.SPOT) and
(not since_ms or since_ms > (date_minus_candles(timeframe, 300).timestamp() * 1000))
):
return 300
return super().ohlcv_candle_limit(timeframe, candle_type, since_ms)
@retrier @retrier
def additional_exchange_init(self) -> None: def additional_exchange_init(self) -> None:
""" """

View File

@ -536,7 +536,8 @@ class FreqtradeBot(LoggingMixin):
if stake_amount is not None and stake_amount > 0.0: if stake_amount is not None and stake_amount > 0.0:
# We should increase our position # We should increase our position
self.execute_entry(trade.pair, stake_amount, trade=trade, is_short=trade.is_short) self.execute_entry(trade.pair, stake_amount, price=current_rate,
trade=trade, is_short=trade.is_short)
if stake_amount is not None and stake_amount < 0.0: if stake_amount is not None and stake_amount < 0.0:
# We should decrease our position # We should decrease our position
@ -586,6 +587,7 @@ class FreqtradeBot(LoggingMixin):
ordertype: Optional[str] = None, ordertype: Optional[str] = None,
enter_tag: Optional[str] = None, enter_tag: Optional[str] = None,
trade: Optional[Trade] = None, trade: Optional[Trade] = None,
order_adjust: bool = False
) -> bool: ) -> bool:
""" """
Executes a limit buy for the given pair Executes a limit buy for the given pair
@ -601,7 +603,7 @@ class FreqtradeBot(LoggingMixin):
pos_adjust = trade is not None pos_adjust = trade is not None
enter_limit_requested, stake_amount, leverage = self.get_valid_enter_price_and_stake( enter_limit_requested, stake_amount, leverage = self.get_valid_enter_price_and_stake(
pair, price, stake_amount, trade_side, enter_tag, trade) pair, price, stake_amount, trade_side, enter_tag, trade, order_adjust)
if not stake_amount: if not stake_amount:
return False return False
@ -746,23 +748,26 @@ class FreqtradeBot(LoggingMixin):
self, pair: str, price: Optional[float], stake_amount: float, self, pair: str, price: Optional[float], stake_amount: float,
trade_side: LongShort, trade_side: LongShort,
entry_tag: Optional[str], entry_tag: Optional[str],
trade: Optional[Trade] trade: Optional[Trade],
order_adjust: bool,
) -> Tuple[float, float, float]: ) -> Tuple[float, float, float]:
if price: if price:
enter_limit_requested = price enter_limit_requested = price
else: else:
# Calculate price # Calculate price
proposed_enter_rate = self.exchange.get_rate( enter_limit_requested = self.exchange.get_rate(
pair, side='entry', is_short=(trade_side == 'short'), refresh=True) pair, side='entry', is_short=(trade_side == 'short'), refresh=True)
if not order_adjust:
# Don't call custom_entry_price in order-adjust scenario
custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price, custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price,
default_retval=proposed_enter_rate)( default_retval=enter_limit_requested)(
pair=pair, current_time=datetime.now(timezone.utc), pair=pair, current_time=datetime.now(timezone.utc),
proposed_rate=proposed_enter_rate, entry_tag=entry_tag, proposed_rate=enter_limit_requested, entry_tag=entry_tag,
side=trade_side, side=trade_side,
) )
enter_limit_requested = self.get_valid_price(custom_entry_price, proposed_enter_rate) enter_limit_requested = self.get_valid_price(custom_entry_price, enter_limit_requested)
if not enter_limit_requested: if not enter_limit_requested:
raise PricingError('Could not determine entry price.') raise PricingError('Could not determine entry price.')
@ -1212,7 +1217,8 @@ class FreqtradeBot(LoggingMixin):
stake_amount=(order_obj.remaining * order_obj.price), stake_amount=(order_obj.remaining * order_obj.price),
price=adjusted_entry_price, price=adjusted_entry_price,
trade=trade, trade=trade,
is_short=trade.is_short is_short=trade.is_short,
order_adjust=True,
) )
def cancel_all_open_orders(self) -> None: def cancel_all_open_orders(self) -> None:
@ -1408,6 +1414,7 @@ class FreqtradeBot(LoggingMixin):
open_date=trade.open_date_utc, open_date=trade.open_date_utc,
) )
exit_type = 'exit' exit_type = 'exit'
exit_reason = exit_tag or exit_check.exit_reason
if exit_check.exit_type in (ExitType.STOP_LOSS, ExitType.TRAILING_STOP_LOSS): if exit_check.exit_type in (ExitType.STOP_LOSS, ExitType.TRAILING_STOP_LOSS):
exit_type = 'stoploss' exit_type = 'stoploss'
@ -1425,7 +1432,7 @@ class FreqtradeBot(LoggingMixin):
pair=trade.pair, trade=trade, pair=trade.pair, trade=trade,
current_time=datetime.now(timezone.utc), current_time=datetime.now(timezone.utc),
proposed_rate=proposed_limit_rate, current_profit=current_profit, proposed_rate=proposed_limit_rate, current_profit=current_profit,
exit_tag=exit_check.exit_reason) exit_tag=exit_reason)
limit = self.get_valid_price(custom_exit_price, proposed_limit_rate) limit = self.get_valid_price(custom_exit_price, proposed_limit_rate)
@ -1442,8 +1449,8 @@ class FreqtradeBot(LoggingMixin):
if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)( if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)(
pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit, pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit,
time_in_force=time_in_force, exit_reason=exit_check.exit_reason, time_in_force=time_in_force, exit_reason=exit_reason,
sell_reason=exit_check.exit_reason, # sellreason -> compatibility sell_reason=exit_reason, # sellreason -> compatibility
current_time=datetime.now(timezone.utc)): current_time=datetime.now(timezone.utc)):
logger.info(f"User requested abortion of exiting {trade.pair}") logger.info(f"User requested abortion of exiting {trade.pair}")
return False return False
@ -1472,7 +1479,7 @@ class FreqtradeBot(LoggingMixin):
trade.open_order_id = order['id'] trade.open_order_id = order['id']
trade.exit_order_status = '' trade.exit_order_status = ''
trade.close_rate_requested = limit trade.close_rate_requested = limit
trade.exit_reason = exit_tag or exit_check.exit_reason trade.exit_reason = exit_reason
# Lock pair for one candle to prevent immediate re-trading # Lock pair for one candle to prevent immediate re-trading
self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc), self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),

View File

@ -535,6 +535,7 @@ class Backtesting:
if exit_.exit_flag: if exit_.exit_flag:
trade.close_date = exit_candle_time trade.close_date = exit_candle_time
exit_reason = exit_.exit_reason
trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60) trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60)
try: try:
@ -545,6 +546,15 @@ class Backtesting:
current_profit = trade.calc_profit_ratio(close_rate) current_profit = trade.calc_profit_ratio(close_rate)
order_type = self.strategy.order_types['exit'] order_type = self.strategy.order_types['exit']
if exit_.exit_type in (ExitType.EXIT_SIGNAL, ExitType.CUSTOM_EXIT): if exit_.exit_type in (ExitType.EXIT_SIGNAL, ExitType.CUSTOM_EXIT):
# Checks and adds an exit tag, after checking that the length of the
# row has the length for an exit tag column
if(
len(row) > EXIT_TAG_IDX
and row[EXIT_TAG_IDX] is not None
and len(row[EXIT_TAG_IDX]) > 0
and exit_.exit_type in (ExitType.EXIT_SIGNAL,)
):
exit_reason = row[EXIT_TAG_IDX]
# Custom exit pricing only for exit-signals # Custom exit pricing only for exit-signals
if order_type == 'limit': if order_type == 'limit':
close_rate = strategy_safe_wrapper(self.strategy.custom_exit_price, close_rate = strategy_safe_wrapper(self.strategy.custom_exit_price,
@ -552,7 +562,7 @@ class Backtesting:
pair=trade.pair, trade=trade, pair=trade.pair, trade=trade,
current_time=exit_candle_time, current_time=exit_candle_time,
proposed_rate=close_rate, current_profit=current_profit, proposed_rate=close_rate, current_profit=current_profit,
exit_tag=exit_.exit_reason) exit_tag=exit_reason)
# We can't place orders lower than current low. # We can't place orders lower than current low.
# freqtrade does not support this in live, and the order would fill immediately # freqtrade does not support this in live, and the order would fill immediately
if trade.is_short: if trade.is_short:
@ -566,22 +576,12 @@ class Backtesting:
pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount, pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount,
rate=close_rate, rate=close_rate,
time_in_force=time_in_force, time_in_force=time_in_force,
sell_reason=exit_.exit_reason, # deprecated sell_reason=exit_reason, # deprecated
exit_reason=exit_.exit_reason, exit_reason=exit_reason,
current_time=exit_candle_time): current_time=exit_candle_time):
return None return None
trade.exit_reason = exit_.exit_reason trade.exit_reason = exit_reason
# Checks and adds an exit tag, after checking that the length of the
# row has the length for an exit tag column
if(
len(row) > EXIT_TAG_IDX
and row[EXIT_TAG_IDX] is not None
and len(row[EXIT_TAG_IDX]) > 0
and exit_.exit_type in (ExitType.EXIT_SIGNAL,)
):
trade.exit_reason = row[EXIT_TAG_IDX]
self.order_id_counter += 1 self.order_id_counter += 1
order = Order( order = Order(
@ -812,11 +812,11 @@ class Backtesting:
remaining=amount, remaining=amount,
cost=stake_amount + trade.fee_open, cost=stake_amount + trade.fee_open,
) )
trade.orders.append(order)
if pos_adjust and self._get_order_filled(order.price, row): if pos_adjust and self._get_order_filled(order.price, row):
order.close_bt_order(current_time, trade) order.close_bt_order(current_time, trade)
else: else:
trade.open_order_id = str(self.order_id_counter) trade.open_order_id = str(self.order_id_counter)
trade.orders.append(order)
trade.recalc_trade_from_orders() trade.recalc_trade_from_orders()
return trade return trade

View File

@ -153,6 +153,7 @@ class Order(_DECL_BASE):
and len(trade.select_filled_orders(trade.entry_side)) == 1): and len(trade.select_filled_orders(trade.entry_side)) == 1):
trade.open_rate = self.price trade.open_rate = self.price
trade.recalc_open_trade_value() trade.recalc_open_trade_value()
trade.adjust_stop_loss(trade.open_rate, trade.stop_loss_pct, refresh=True)
@staticmethod @staticmethod
def update_orders(orders: List['Order'], order: Dict[str, Any]): def update_orders(orders: List['Order'], order: Dict[str, Any]):
@ -491,7 +492,7 @@ class LocalTrade():
self.stoploss_last_update = datetime.utcnow() self.stoploss_last_update = datetime.utcnow()
def adjust_stop_loss(self, current_price: float, stoploss: float, def adjust_stop_loss(self, current_price: float, stoploss: float,
initial: bool = False) -> None: initial: bool = False, refresh: bool = False) -> None:
""" """
This adjusts the stop loss to it's most recently observed setting This adjusts the stop loss to it's most recently observed setting
:param current_price: Current rate the asset is traded :param current_price: Current rate the asset is traded
@ -502,6 +503,7 @@ class LocalTrade():
if initial and not (self.stop_loss is None or self.stop_loss == 0): if initial and not (self.stop_loss is None or self.stop_loss == 0):
# Don't modify if called with initial and nothing to do # Don't modify if called with initial and nothing to do
return return
refresh = True if refresh and self.nr_of_successful_entries == 1 else False
leverage = self.leverage or 1.0 leverage = self.leverage or 1.0
if self.is_short: if self.is_short:
@ -516,8 +518,7 @@ class LocalTrade():
new_loss = max(self.liquidation_price, new_loss) new_loss = max(self.liquidation_price, new_loss)
# no stop loss assigned yet # no stop loss assigned yet
if self.initial_stop_loss_pct is None: if self.initial_stop_loss_pct is None or refresh:
logger.debug(f"{self.pair} - Assigning new stoploss...")
self._set_stop_loss(new_loss, stoploss) self._set_stop_loss(new_loss, stoploss)
self.initial_stop_loss = new_loss self.initial_stop_loss = new_loss
self.initial_stop_loss_pct = -1 * abs(stoploss) self.initial_stop_loss_pct = -1 * abs(stoploss)
@ -656,7 +657,7 @@ class LocalTrade():
def recalc_open_trade_value(self) -> None: def recalc_open_trade_value(self) -> None:
""" """
Recalculate open_trade_value. Recalculate open_trade_value.
Must be called whenever open_rate, fee_open or is_short is changed. Must be called whenever open_rate, fee_open is changed.
""" """
self.open_trade_value = self._calc_open_trade_value() self.open_trade_value = self._calc_open_trade_value()

View File

@ -32,18 +32,19 @@ class AgeFilter(IPairList):
self._min_days_listed = pairlistconfig.get('min_days_listed', 10) self._min_days_listed = pairlistconfig.get('min_days_listed', 10)
self._max_days_listed = pairlistconfig.get('max_days_listed', None) self._max_days_listed = pairlistconfig.get('max_days_listed', None)
candle_limit = exchange.ohlcv_candle_limit('1d', self._config['candle_type_def'])
if self._min_days_listed < 1: if self._min_days_listed < 1:
raise OperationalException("AgeFilter requires min_days_listed to be >= 1") raise OperationalException("AgeFilter requires min_days_listed to be >= 1")
if self._min_days_listed > exchange.ohlcv_candle_limit('1d'): if self._min_days_listed > candle_limit:
raise OperationalException("AgeFilter requires min_days_listed to not exceed " raise OperationalException("AgeFilter requires min_days_listed to not exceed "
"exchange max request size " "exchange max request size "
f"({exchange.ohlcv_candle_limit('1d')})") f"({candle_limit})")
if self._max_days_listed and self._max_days_listed <= self._min_days_listed: if self._max_days_listed and self._max_days_listed <= self._min_days_listed:
raise OperationalException("AgeFilter max_days_listed <= min_days_listed not permitted") raise OperationalException("AgeFilter max_days_listed <= min_days_listed not permitted")
if self._max_days_listed and self._max_days_listed > exchange.ohlcv_candle_limit('1d'): if self._max_days_listed and self._max_days_listed > candle_limit:
raise OperationalException("AgeFilter requires max_days_listed to not exceed " raise OperationalException("AgeFilter requires max_days_listed to not exceed "
"exchange max request size " "exchange max request size "
f"({exchange.ohlcv_candle_limit('1d')})") f"({candle_limit})")
@property @property
def needstickers(self) -> bool: def needstickers(self) -> bool:

View File

@ -19,6 +19,7 @@ class OffsetFilter(IPairList):
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
self._offset = pairlistconfig.get('offset', 0) self._offset = pairlistconfig.get('offset', 0)
self._number_pairs = pairlistconfig.get('number_assets', 0)
if self._offset < 0: if self._offset < 0:
raise OperationalException("OffsetFilter requires offset to be >= 0") raise OperationalException("OffsetFilter requires offset to be >= 0")
@ -36,7 +37,9 @@ class OffsetFilter(IPairList):
""" """
Short whitelist method description - used for startup-messages Short whitelist method description - used for startup-messages
""" """
return f"{self.name} - Offseting pairs by {self._offset}." if self._number_pairs:
return f"{self.name} - Taking {self._number_pairs} Pairs, starting from {self._offset}."
return f"{self.name} - Offsetting pairs by {self._offset}."
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
""" """
@ -50,5 +53,9 @@ class OffsetFilter(IPairList):
self.log_once(f"Offset of {self._offset} is larger than " + self.log_once(f"Offset of {self._offset} is larger than " +
f"pair count of {len(pairlist)}", logger.warning) f"pair count of {len(pairlist)}", logger.warning)
pairs = pairlist[self._offset:] pairs = pairlist[self._offset:]
if self._number_pairs:
pairs = pairs[:self._number_pairs]
self.log_once(f"Searching {len(pairs)} pairs: {pairs}", logger.info) self.log_once(f"Searching {len(pairs)} pairs: {pairs}", logger.info)
return pairs return pairs

View File

@ -38,12 +38,12 @@ class VolatilityFilter(IPairList):
self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period) self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period)
candle_limit = exchange.ohlcv_candle_limit('1d', self._config['candle_type_def'])
if self._days < 1: if self._days < 1:
raise OperationalException("VolatilityFilter requires lookback_days to be >= 1") raise OperationalException("VolatilityFilter requires lookback_days to be >= 1")
if self._days > exchange.ohlcv_candle_limit('1d'): if self._days > candle_limit:
raise OperationalException("VolatilityFilter requires lookback_days to not " raise OperationalException("VolatilityFilter requires lookback_days to not "
"exceed exchange max request size " f"exceed exchange max request size ({candle_limit})")
f"({exchange.ohlcv_candle_limit('1d')})")
@property @property
def needstickers(self) -> bool: def needstickers(self) -> bool:

View File

@ -84,12 +84,13 @@ class VolumePairList(IPairList):
raise OperationalException( raise OperationalException(
f'key {self._sort_key} not in {SORT_VALUES}') f'key {self._sort_key} not in {SORT_VALUES}')
candle_limit = exchange.ohlcv_candle_limit(
self._lookback_timeframe, self._config['candle_type_def'])
if self._lookback_period < 0: if self._lookback_period < 0:
raise OperationalException("VolumeFilter requires lookback_period to be >= 0") raise OperationalException("VolumeFilter requires lookback_period to be >= 0")
if self._lookback_period > exchange.ohlcv_candle_limit(self._lookback_timeframe): if self._lookback_period > candle_limit:
raise OperationalException("VolumeFilter requires lookback_period to not " raise OperationalException("VolumeFilter requires lookback_period to not "
"exceed exchange max request size " f"exceed exchange max request size ({candle_limit})")
f"({exchange.ohlcv_candle_limit(self._lookback_timeframe)})")
@property @property
def needstickers(self) -> bool: def needstickers(self) -> bool:

View File

@ -33,12 +33,12 @@ class RangeStabilityFilter(IPairList):
self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period) self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period)
candle_limit = exchange.ohlcv_candle_limit('1d', self._config['candle_type_def'])
if self._days < 1: if self._days < 1:
raise OperationalException("RangeStabilityFilter requires lookback_days to be >= 1") raise OperationalException("RangeStabilityFilter requires lookback_days to be >= 1")
if self._days > exchange.ohlcv_candle_limit('1d'): if self._days > candle_limit:
raise OperationalException("RangeStabilityFilter requires lookback_days to not " raise OperationalException("RangeStabilityFilter requires lookback_days to not "
"exceed exchange max request size " f"exceed exchange max request size ({candle_limit})")
f"({exchange.ohlcv_candle_limit('1d')})")
@property @property
def needstickers(self) -> bool: def needstickers(self) -> bool:

View File

@ -1410,14 +1410,14 @@ class Telegram(RPCHandler):
"Optionally takes a rate at which to sell " "Optionally takes a rate at which to sell "
"(only applies to limit orders).` \n") "(only applies to limit orders).` \n")
message = ( message = (
"_BotControl_\n" "_Bot Control_\n"
"------------\n" "------------\n"
"*/start:* `Starts the trader`\n" "*/start:* `Starts the trader`\n"
"*/stop:* Stops the trader\n" "*/stop:* Stops the trader\n"
"*/stopbuy:* `Stops buying, but handles open trades gracefully` \n" "*/stopbuy:* `Stops buying, but handles open trades gracefully` \n"
"*/forceexit <trade_id>|all:* `Instantly exits the given trade or all trades, " "*/forceexit <trade_id>|all:* `Instantly exits the given trade or all trades, "
"regardless of profit`\n" "regardless of profit`\n"
"*/fe <trade_id>|all:* `Alias to /forceexit`" "*/fe <trade_id>|all:* `Alias to /forceexit`\n"
f"{force_enter_text if self._config.get('force_entry_enable', False) else ''}" f"{force_enter_text if self._config.get('force_entry_enable', False) else ''}"
"*/delete <trade_id>:* `Instantly delete the given trade in the database`\n" "*/delete <trade_id>:* `Instantly delete the given trade in the database`\n"
"*/whitelist:* `Show current whitelist` \n" "*/whitelist:* `Show current whitelist` \n"

View File

@ -6,7 +6,7 @@
coveralls==3.3.1 coveralls==3.3.1
flake8==4.0.1 flake8==4.0.1
flake8-tidy-imports==4.7.0 flake8-tidy-imports==4.8.0
mypy==0.950 mypy==0.950
pre-commit==2.19.0 pre-commit==2.19.0
pytest==7.1.2 pytest==7.1.2
@ -16,7 +16,7 @@ pytest-mock==3.7.0
pytest-random-order==1.0.4 pytest-random-order==1.0.4
isort==5.10.1 isort==5.10.1
# For datetime mocking # For datetime mocking
time-machine==2.6.0 time-machine==2.7.0
# Convert jupyter notebooks to markdown documents # Convert jupyter notebooks to markdown documents
nbconvert==6.5.0 nbconvert==6.5.0

View File

@ -3,7 +3,7 @@
# Required for hyperopt # Required for hyperopt
scipy==1.8.0 scipy==1.8.0
scikit-learn==1.0.2 scikit-learn==1.1.0
scikit-optimize==0.9.0 scikit-optimize==0.9.0
filelock==3.6.0 filelock==3.7.0
progressbar2==4.0.0 progressbar2==4.0.0

View File

@ -1,4 +1,4 @@
# Include all requirements to run the bot. # Include all requirements to run the bot.
-r requirements.txt -r requirements.txt
plotly==5.7.0 plotly==5.8.0

View File

@ -2,7 +2,7 @@ numpy==1.22.3
pandas==1.4.2 pandas==1.4.2
pandas-ta==0.3.14b pandas-ta==0.3.14b
ccxt==1.81.81 ccxt==1.82.61
# Pin cryptography for now due to rust build errors with piwheels # Pin cryptography for now due to rust build errors with piwheels
cryptography==37.0.2 cryptography==37.0.2
aiohttp==3.8.1 aiohttp==3.8.1
@ -34,9 +34,9 @@ orjson==3.6.8
sdnotify==0.3.2 sdnotify==0.3.2
# API Server # API Server
fastapi==0.76.0 fastapi==0.78.0
uvicorn==0.17.6 uvicorn==0.17.6
pyjwt==2.3.0 pyjwt==2.4.0
aiofiles==0.8.0 aiofiles==0.8.0
psutil==5.9.0 psutil==5.9.0

View File

@ -32,7 +32,7 @@ tests_require =
pytest-mock pytest-mock
packages = find: packages = find:
python_requires = >=3.6 python_requires = >=3.8
[options.entry_points] [options.entry_points]
console_scripts = console_scripts =

View File

@ -25,7 +25,7 @@ function check_installed_python() {
exit 2 exit 2
fi fi
for v in 9 10 8 for v in 10 9 8
do do
PYTHON="python3.${v}" PYTHON="python3.${v}"
which $PYTHON which $PYTHON

View File

@ -835,6 +835,23 @@ def test_download_data_trades(mocker, caplog):
start_download_data(pargs) start_download_data(pargs)
def test_download_data_data_invalid(mocker):
patch_exchange(mocker, id="kraken")
mocker.patch(
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value={})
)
args = [
"download-data",
"--exchange", "kraken",
"--pairs", "ETH/BTC", "XRP/BTC",
"--days", "20",
]
pargs = get_args(args)
pargs['config'] = None
with pytest.raises(OperationalException, match=r"Historic klines not available for .*"):
start_download_data(pargs)
def test_start_convert_trades(mocker, caplog): def test_start_convert_trades(mocker, caplog):
convert_mock = mocker.patch('freqtrade.commands.data_commands.convert_trades_to_ohlcv', convert_mock = mocker.patch('freqtrade.commands.data_commands.convert_trades_to_ohlcv',
MagicMock(return_value=[])) MagicMock(return_value=[]))

View File

@ -13,6 +13,7 @@ import pytest
from freqtrade.enums import CandleType from freqtrade.enums import CandleType
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date
from freqtrade.exchange.exchange import timeframe_to_msecs
from freqtrade.resolvers.exchange_resolver import ExchangeResolver from freqtrade.resolvers.exchange_resolver import ExchangeResolver
from tests.conftest import get_default_conf_usdt from tests.conftest import get_default_conf_usdt
@ -219,7 +220,7 @@ class TestCCXTExchange():
assert len(l2['asks']) == next_limit assert len(l2['asks']) == next_limit
assert len(l2['asks']) == next_limit assert len(l2['asks']) == next_limit
def test_fetch_ohlcv(self, exchange): def test_ccxt_fetch_ohlcv(self, exchange):
exchange, exchangename = exchange exchange, exchangename = exchange
pair = EXCHANGES[exchangename]['pair'] pair = EXCHANGES[exchangename]['pair']
timeframe = EXCHANGES[exchangename]['timeframe'] timeframe = EXCHANGES[exchangename]['timeframe']
@ -231,11 +232,44 @@ class TestCCXTExchange():
assert len(ohlcv[pair_tf]) == len(exchange.klines(pair_tf)) assert len(ohlcv[pair_tf]) == len(exchange.klines(pair_tf))
# assert len(exchange.klines(pair_tf)) > 200 # assert len(exchange.klines(pair_tf)) > 200
# Assume 90% uptime ... # Assume 90% uptime ...
assert len(exchange.klines(pair_tf)) > exchange.ohlcv_candle_limit(timeframe) * 0.90 assert len(exchange.klines(pair_tf)) > exchange.ohlcv_candle_limit(
timeframe, CandleType.SPOT) * 0.90
# Check if last-timeframe is within the last 2 intervals # Check if last-timeframe is within the last 2 intervals
now = datetime.now(timezone.utc) - timedelta(minutes=(timeframe_to_minutes(timeframe) * 2)) now = datetime.now(timezone.utc) - timedelta(minutes=(timeframe_to_minutes(timeframe) * 2))
assert exchange.klines(pair_tf).iloc[-1]['date'] >= timeframe_to_prev_date(timeframe, now) assert exchange.klines(pair_tf).iloc[-1]['date'] >= timeframe_to_prev_date(timeframe, now)
def test_ccxt__async_get_candle_history(self, exchange):
exchange, exchangename = exchange
# For some weired reason, this test returns random lengths for bittrex.
if not exchange._ft_has['ohlcv_has_history'] or exchangename == 'bittrex':
return
pair = EXCHANGES[exchangename]['pair']
timeframe = EXCHANGES[exchangename]['timeframe']
candle_type = CandleType.SPOT
timeframe_ms = timeframe_to_msecs(timeframe)
now = timeframe_to_prev_date(
timeframe, datetime.now(timezone.utc))
for offset in (360, 120, 30, 10, 5, 2):
since = now - timedelta(days=offset)
since_ms = int(since.timestamp() * 1000)
res = exchange.loop.run_until_complete(exchange._async_get_candle_history(
pair=pair,
timeframe=timeframe,
since_ms=since_ms,
candle_type=candle_type
)
)
assert res
assert res[0] == pair
assert res[1] == timeframe
assert res[2] == candle_type
candles = res[3]
candle_count = exchange.ohlcv_candle_limit(timeframe, candle_type, since_ms) * 0.9
candle_count1 = (now.timestamp() * 1000 - since_ms) // timeframe_ms
assert len(candles) >= min(candle_count, candle_count1)
assert candles[0][0] == since_ms or (since_ms + timeframe_ms)
def test_ccxt_fetch_funding_rate_history(self, exchange_futures): def test_ccxt_fetch_funding_rate_history(self, exchange_futures):
exchange, exchangename = exchange_futures exchange, exchangename = exchange_futures
if not exchange: if not exchange:

View File

@ -17,9 +17,9 @@ from freqtrade.exceptions import (DDosProtection, DependencyException, InvalidOr
from freqtrade.exchange import Binance, Bittrex, Exchange, Kraken from freqtrade.exchange import Binance, Bittrex, Exchange, Kraken
from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, API_RETRY_COUNT, from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, API_RETRY_COUNT,
calculate_backoff, remove_credentials) calculate_backoff, remove_credentials)
from freqtrade.exchange.exchange import (market_is_active, timeframe_to_minutes, timeframe_to_msecs, from freqtrade.exchange.exchange import (date_minus_candles, market_is_active, timeframe_to_minutes,
timeframe_to_next_date, timeframe_to_prev_date, timeframe_to_msecs, timeframe_to_next_date,
timeframe_to_seconds) timeframe_to_prev_date, timeframe_to_seconds)
from freqtrade.resolvers.exchange_resolver import ExchangeResolver from freqtrade.resolvers.exchange_resolver import ExchangeResolver
from tests.conftest import get_mock_coro, get_patched_exchange, log_has, log_has_re, num_log_has_re from tests.conftest import get_mock_coro, get_patched_exchange, log_has, log_has_re, num_log_has_re
@ -939,6 +939,7 @@ def test_validate_timeframes_emulated_ohlcvi_2(default_conf, mocker):
def test_validate_timeframes_not_in_config(default_conf, mocker): def test_validate_timeframes_not_in_config(default_conf, mocker):
# TODO: this test does not assert ...
del default_conf["timeframe"] del default_conf["timeframe"]
api_mock = MagicMock() api_mock = MagicMock()
id_mock = PropertyMock(return_value='test_exchange') id_mock = PropertyMock(return_value='test_exchange')
@ -954,6 +955,7 @@ def test_validate_timeframes_not_in_config(default_conf, mocker):
mocker.patch('freqtrade.exchange.Exchange.validate_pairs') mocker.patch('freqtrade.exchange.Exchange.validate_pairs')
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
mocker.patch('freqtrade.exchange.Exchange.validate_pricing') mocker.patch('freqtrade.exchange.Exchange.validate_pricing')
mocker.patch('freqtrade.exchange.Exchange.validate_required_startup_candles')
Exchange(default_conf) Exchange(default_conf)
@ -1084,6 +1086,13 @@ def test_validate_required_startup_candles(default_conf, mocker, caplog):
with pytest.raises(OperationalException, match=r'This strategy requires 6000.*'): with pytest.raises(OperationalException, match=r'This strategy requires 6000.*'):
Exchange(default_conf) Exchange(default_conf)
# Emulate kraken mode
ex._ft_has['ohlcv_has_history'] = False
with pytest.raises(OperationalException,
match=r'This strategy requires 2500.*, '
r'which is more than the amount.*'):
ex.validate_required_startup_candles(2500, '5m')
def test_exchange_has(default_conf, mocker): def test_exchange_has(default_conf, mocker):
exchange = get_patched_exchange(mocker, default_conf) exchange = get_patched_exchange(mocker, default_conf)
@ -1875,7 +1884,7 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name, candle_
exchange._async_get_candle_history = Mock(wraps=mock_candle_hist) exchange._async_get_candle_history = Mock(wraps=mock_candle_hist)
# one_call calculation * 1.8 should do 2 calls # one_call calculation * 1.8 should do 2 calls
since = 5 * 60 * exchange.ohlcv_candle_limit('5m') * 1.8 since = 5 * 60 * exchange.ohlcv_candle_limit('5m', CandleType.SPOT) * 1.8
ret = exchange.get_historic_ohlcv( ret = exchange.get_historic_ohlcv(
pair, pair,
"5m", "5m",
@ -1941,7 +1950,7 @@ def test_get_historic_ohlcv_as_df(default_conf, mocker, exchange_name, candle_ty
exchange._async_get_candle_history = Mock(wraps=mock_candle_hist) exchange._async_get_candle_history = Mock(wraps=mock_candle_hist)
# one_call calculation * 1.8 should do 2 calls # one_call calculation * 1.8 should do 2 calls
since = 5 * 60 * exchange.ohlcv_candle_limit('5m') * 1.8 since = 5 * 60 * exchange.ohlcv_candle_limit('5m', CandleType.SPOT) * 1.8
ret = exchange.get_historic_ohlcv_as_df( ret = exchange.get_historic_ohlcv_as_df(
pair, pair,
"5m", "5m",
@ -1995,7 +2004,7 @@ async def test__async_get_historic_ohlcv(default_conf, mocker, caplog, exchange_
) )
# Required candles # Required candles
candles = (end_ts - start_ts) / 300_000 candles = (end_ts - start_ts) / 300_000
exp = candles // exchange.ohlcv_candle_limit('5m') + 1 exp = candles // exchange.ohlcv_candle_limit('5m', CandleType.SPOT) + 1
# Depending on the exchange, this should be called between 1 and 6 times. # Depending on the exchange, this should be called between 1 and 6 times.
assert exchange._api_async.fetch_ohlcv.call_count == exp assert exchange._api_async.fetch_ohlcv.call_count == exp
@ -3342,7 +3351,7 @@ def test_ohlcv_candle_limit(default_conf, mocker, exchange_name):
expected = exchange._ft_has['ohlcv_candle_limit_per_timeframe'][timeframe] expected = exchange._ft_has['ohlcv_candle_limit_per_timeframe'][timeframe]
# This should only run for bittrex # This should only run for bittrex
assert exchange_name == 'bittrex' assert exchange_name == 'bittrex'
assert exchange.ohlcv_candle_limit(timeframe) == expected assert exchange.ohlcv_candle_limit(timeframe, CandleType.SPOT) == expected
def test_timeframe_to_minutes(): def test_timeframe_to_minutes():
@ -3424,6 +3433,17 @@ def test_timeframe_to_next_date():
assert timeframe_to_next_date("5m", date) == date + timedelta(minutes=5) assert timeframe_to_next_date("5m", date) == date + timedelta(minutes=5)
def test_date_minus_candles():
date = datetime(2019, 8, 12, 13, 25, 0, tzinfo=timezone.utc)
assert date_minus_candles("5m", 3, date) == date - timedelta(minutes=15)
assert date_minus_candles("5m", 5, date) == date - timedelta(minutes=25)
assert date_minus_candles("1m", 6, date) == date - timedelta(minutes=6)
assert date_minus_candles("1h", 3, date) == date - timedelta(hours=3, minutes=25)
assert date_minus_candles("1h", 3) == timeframe_to_prev_date('1h') - timedelta(hours=3)
@pytest.mark.parametrize( @pytest.mark.parametrize(
"market_symbol,base,quote,exchange,spot,margin,futures,trademode,add_dict,expected_result", "market_symbol,base,quote,exchange,spot,margin,futures,trademode,add_dict,expected_result",
[ [

View File

@ -1,12 +1,42 @@
from datetime import datetime, timedelta, timezone
from unittest.mock import MagicMock, PropertyMock from unittest.mock import MagicMock, PropertyMock
import pytest import pytest
from freqtrade.enums import MarginMode, TradingMode from freqtrade.enums import MarginMode, TradingMode
from freqtrade.enums.candletype import CandleType
from freqtrade.exchange.exchange import timeframe_to_minutes
from tests.conftest import get_patched_exchange from tests.conftest import get_patched_exchange
from tests.exchange.test_exchange import ccxt_exceptionhandlers from tests.exchange.test_exchange import ccxt_exceptionhandlers
def test_okx_ohlcv_candle_limit(default_conf, mocker):
exchange = get_patched_exchange(mocker, default_conf, id='okx')
timeframes = ('1m', '5m', '1h')
start_time = int(datetime(2021, 1, 1, tzinfo=timezone.utc).timestamp() * 1000)
for timeframe in timeframes:
assert exchange.ohlcv_candle_limit(timeframe, CandleType.SPOT) == 300
assert exchange.ohlcv_candle_limit(timeframe, CandleType.FUTURES) == 300
assert exchange.ohlcv_candle_limit(timeframe, CandleType.MARK) == 100
assert exchange.ohlcv_candle_limit(timeframe, CandleType.FUNDING_RATE) == 100
assert exchange.ohlcv_candle_limit(timeframe, CandleType.SPOT, start_time) == 100
assert exchange.ohlcv_candle_limit(timeframe, CandleType.FUTURES, start_time) == 100
assert exchange.ohlcv_candle_limit(timeframe, CandleType.MARK, start_time) == 100
assert exchange.ohlcv_candle_limit(timeframe, CandleType.FUNDING_RATE, start_time) == 100
one_call = int((datetime.now(timezone.utc) - timedelta(
minutes=290 * timeframe_to_minutes(timeframe))).timestamp() * 1000)
assert exchange.ohlcv_candle_limit(timeframe, CandleType.SPOT, one_call) == 300
assert exchange.ohlcv_candle_limit(timeframe, CandleType.FUTURES, one_call) == 300
one_call = int((datetime.now(timezone.utc) - timedelta(
minutes=320 * timeframe_to_minutes(timeframe))).timestamp() * 1000)
assert exchange.ohlcv_candle_limit(timeframe, CandleType.SPOT, one_call) == 100
assert exchange.ohlcv_candle_limit(timeframe, CandleType.FUTURES, one_call) == 100
def test_get_maintenance_ratio_and_amt_okx( def test_get_maintenance_ratio_and_amt_okx(
default_conf, default_conf,
mocker, mocker,

View File

@ -522,7 +522,7 @@ tc32 = BTContainer(data=[
trailing_stop_positive=0.03, trailing_stop_positive=0.03,
trades=[ trades=[
BTrade(exit_reason=ExitType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3, is_short=True) BTrade(exit_reason=ExitType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3, is_short=True)
] ]
) )
# Test 33: trailing_stop should be triggered by low of next candle, without adjusting stoploss using # Test 33: trailing_stop should be triggered by low of next candle, without adjusting stoploss using
@ -662,7 +662,7 @@ tc41 = BTContainer(data=[
custom_entry_price=4000, custom_entry_price=4000,
trades=[ trades=[
BTrade(exit_reason=ExitType.STOP_LOSS, open_tick=1, close_tick=1, is_short=True) BTrade(exit_reason=ExitType.STOP_LOSS, open_tick=1, close_tick=1, is_short=True)
] ]
) )
# Test 42: Custom-entry-price around candle low # Test 42: Custom-entry-price around candle low
@ -762,7 +762,7 @@ tc48 = BTContainer(data=[
[2, 4900, 5250, 4500, 5100, 6172, 0, 0], # Order readjust [2, 4900, 5250, 4500, 5100, 6172, 0, 0], # Order readjust
[3, 5100, 5100, 4650, 4750, 6172, 0, 1], [3, 5100, 5100, 4650, 4750, 6172, 0, 1],
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]], [4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
stop_loss=-0.01, roi={"0": 0.10}, profit_perc=-0.087, stop_loss=-0.2, roi={"0": 0.10}, profit_perc=-0.087,
use_exit_signal=True, timeout=1000, use_exit_signal=True, timeout=1000,
custom_entry_price=4200, adjust_entry_price=5200, custom_entry_price=4200, adjust_entry_price=5200,
trades=[BTrade(exit_reason=ExitType.EXIT_SIGNAL, open_tick=1, close_tick=4, is_short=False)] trades=[BTrade(exit_reason=ExitType.EXIT_SIGNAL, open_tick=1, close_tick=4, is_short=False)]
@ -777,7 +777,7 @@ tc49 = BTContainer(data=[
[2, 4900, 5250, 4900, 5100, 6172, 0, 0, 0, 0], # Order readjust [2, 4900, 5250, 4900, 5100, 6172, 0, 0, 0, 0], # Order readjust
[3, 5100, 5100, 4650, 4750, 6172, 0, 0, 0, 1], [3, 5100, 5100, 4650, 4750, 6172, 0, 0, 0, 1],
[4, 4750, 4950, 4350, 4750, 6172, 0, 0, 0, 0]], [4, 4750, 4950, 4350, 4750, 6172, 0, 0, 0, 0]],
stop_loss=-0.01, roi={"0": 0.10}, profit_perc=0.05, stop_loss=-0.2, roi={"0": 0.10}, profit_perc=0.05,
use_exit_signal=True, timeout=1000, use_exit_signal=True, timeout=1000,
custom_entry_price=5300, adjust_entry_price=5000, custom_entry_price=5300, adjust_entry_price=5000,
trades=[BTrade(exit_reason=ExitType.EXIT_SIGNAL, open_tick=1, close_tick=4, is_short=True)] trades=[BTrade(exit_reason=ExitType.EXIT_SIGNAL, open_tick=1, close_tick=4, is_short=True)]
@ -811,6 +811,35 @@ tc51 = BTContainer(data=[
trades=[] trades=[]
) )
# Test 52: Custom-entry-price below all candles - readjust order - stoploss
tc52 = BTContainer(data=[
# D O H L C V EL XL ES Xs BT
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
[1, 5000, 5500, 4951, 5000, 6172, 0, 0], # enter trade (signal on last candle)
[2, 4900, 5250, 4500, 5100, 6172, 0, 0], # Order readjust
[3, 5100, 5100, 4650, 4750, 6172, 0, 0], # stoploss hit?
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
stop_loss=-0.03, roi={"0": 0.10}, profit_perc=-0.03,
use_exit_signal=True, timeout=1000,
custom_entry_price=4200, adjust_entry_price=5200,
trades=[BTrade(exit_reason=ExitType.STOP_LOSS, open_tick=1, close_tick=2, is_short=False)]
)
# Test 53: Custom-entry-price short above all candles - readjust order - stoploss
tc53 = BTContainer(data=[
# D O H L C V EL XL ES Xs BT
[0, 5000, 5050, 4950, 5000, 6172, 0, 0, 1, 0],
[1, 5000, 5200, 4951, 5000, 6172, 0, 0, 0, 0], # enter trade (signal on last candle)
[2, 4900, 5250, 4900, 5100, 6172, 0, 0, 0, 0], # Order readjust
[3, 5100, 5100, 4650, 4750, 6172, 0, 0, 0, 1], # stoploss hit?
[4, 4750, 4950, 4350, 4750, 6172, 0, 0, 0, 0]],
stop_loss=-0.03, roi={"0": 0.10}, profit_perc=-0.03,
use_exit_signal=True, timeout=1000,
custom_entry_price=5300, adjust_entry_price=5000,
trades=[BTrade(exit_reason=ExitType.STOP_LOSS, open_tick=1, close_tick=2, is_short=True)]
)
TESTS = [ TESTS = [
tc0, tc0,
tc1, tc1,
@ -864,6 +893,8 @@ TESTS = [
tc49, tc49,
tc50, tc50,
tc51, tc51,
tc52,
tc53,
] ]
@ -933,3 +964,5 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data: BTContainer)
assert res.open_date == _get_frame_time_from_offset(trade.open_tick) assert res.open_date == _get_frame_time_from_offset(trade.open_tick)
assert res.close_date == _get_frame_time_from_offset(trade.close_tick) assert res.close_date == _get_frame_time_from_offset(trade.close_tick)
assert res.is_short == trade.is_short assert res.is_short == trade.is_short
backtesting.cleanup()
del backtesting

View File

@ -470,12 +470,16 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
"BTC", ['ETH/BTC', 'TKN/BTC']), "BTC", ['ETH/BTC', 'TKN/BTC']),
# VolumePairList with no offset = unchanged pairlist # VolumePairList with no offset = unchanged pairlist
([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"}, ([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"},
{"method": "OffsetFilter", "offset": 0}], {"method": "OffsetFilter", "offset": 0, "number_assets": 0}],
"USDT", ['ETH/USDT', 'NANO/USDT', 'ADAHALF/USDT', 'ADADOUBLE/USDT']), "USDT", ['ETH/USDT', 'NANO/USDT', 'ADAHALF/USDT', 'ADADOUBLE/USDT']),
# VolumePairList with offset = 2 # VolumePairList with offset = 2
([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"}, ([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"},
{"method": "OffsetFilter", "offset": 2}], {"method": "OffsetFilter", "offset": 2}],
"USDT", ['ADAHALF/USDT', 'ADADOUBLE/USDT']), "USDT", ['ADAHALF/USDT', 'ADADOUBLE/USDT']),
# VolumePairList with offset and limit
([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"},
{"method": "OffsetFilter", "offset": 1, "number_assets": 2}],
"USDT", ['NANO/USDT', 'ADAHALF/USDT']),
# VolumePairList with higher offset, than total pairlist # VolumePairList with higher offset, than total pairlist
([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"}, ([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"},
{"method": "OffsetFilter", "offset": 100}], {"method": "OffsetFilter", "offset": 100}],
@ -1152,6 +1156,10 @@ def test_spreadfilter_invalid_data(mocker, default_conf, markets, tickers, caplo
"0.01 and above 0.99 over the last days.'}]", "0.01 and above 0.99 over the last days.'}]",
None None
), ),
({"method": "OffsetFilter", "offset": 5, "number_assets": 10},
"[{'OffsetFilter': 'OffsetFilter - Taking 10 Pairs, starting from 5.'}]",
None
),
]) ])
def test_pricefilter_desc(mocker, whitelist_conf, markets, pairlistconfig, def test_pricefilter_desc(mocker, whitelist_conf, markets, pairlistconfig,
desc_expected, exception_expected): desc_expected, exception_expected):

View File

@ -372,11 +372,15 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, fee, mocker) -> None:
freqtrade.enter_positions() freqtrade.enter_positions()
assert len(Trade.get_trades().all()) == 1 assert len(Trade.get_trades().all()) == 1
trade = Trade.get_trades().first() trade: Trade = Trade.get_trades().first()
assert len(trade.orders) == 1 assert len(trade.orders) == 1
assert trade.open_order_id is not None assert trade.open_order_id is not None
assert pytest.approx(trade.stake_amount) == 60 assert pytest.approx(trade.stake_amount) == 60
assert trade.open_rate == 1.96 assert trade.open_rate == 1.96
assert trade.stop_loss_pct is None
assert trade.stop_loss == 0.0
assert trade.initial_stop_loss == 0.0
assert trade.initial_stop_loss_pct is None
# No adjustment # No adjustment
freqtrade.process() freqtrade.process()
trade = Trade.get_trades().first() trade = Trade.get_trades().first()
@ -392,6 +396,10 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, fee, mocker) -> None:
assert trade.open_order_id is not None assert trade.open_order_id is not None
# Open rate is not adjusted yet # Open rate is not adjusted yet
assert trade.open_rate == 1.96 assert trade.open_rate == 1.96
assert trade.stop_loss_pct is None
assert trade.stop_loss == 0.0
assert trade.initial_stop_loss == 0.0
assert trade.initial_stop_loss_pct is None
# Fill order # Fill order
mocker.patch('freqtrade.exchange.Exchange._is_dry_limit_order_filled', return_value=True) mocker.patch('freqtrade.exchange.Exchange._is_dry_limit_order_filled', return_value=True)
@ -401,6 +409,10 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, fee, mocker) -> None:
assert trade.open_order_id is None assert trade.open_order_id is None
# Open rate is not adjusted yet # Open rate is not adjusted yet
assert trade.open_rate == 1.99 assert trade.open_rate == 1.99
assert trade.stop_loss_pct == -0.1
assert trade.stop_loss == 1.99 * 0.9
assert trade.initial_stop_loss == 1.99 * 0.9
assert trade.initial_stop_loss_pct == -0.1
# 2nd order - not filling # 2nd order - not filling
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=120) freqtrade.strategy.adjust_trade_position = MagicMock(return_value=120)