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 }}
strategy:
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"]
steps:
@ -70,7 +70,7 @@ jobs:
if: matrix.python-version == '3.9'
- name: Coveralls
if: (runner.os == 'Linux' && matrix.python-version == '3.8')
if: (runner.os == 'Linux' && matrix.python-version == '3.9')
env:
# Coveralls token. Not used as secret due to github not providing secrets to forked repositories
COVERALLS_REPO_TOKEN: 6D1m0xupS3FgutfuGao8keFf9Hc0FpIXu
@ -157,24 +157,9 @@ jobs:
pip install -e .
- name: Tests
if: (runner.os != 'Linux' || matrix.python-version != '3.8')
run: |
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
run: |
cp config_examples/config_bittrex.example.json config.json
@ -273,7 +258,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: 3.9
python-version: "3.10"
- name: pre-commit dependencies
run: |
@ -292,7 +277,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: 3.9
python-version: "3.10"
- name: Documentation build
run: |
@ -358,7 +343,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: 3.8
python-version: "3.9"
- name: Extract branch name
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
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
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.
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.
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.
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
"pairlists": [
// ...
{
"method": "OffsetFilter",
"offset": 10
"offset": 10,
"number_assets": 20
}
],
```

View File

@ -1,5 +1,5 @@
mkdocs==1.3.0
mkdocs-material==8.2.14
mkdocs-material==8.2.15
mdx_truly_sane_lists==1.2
pymdown-extensions==9.4
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'],
)
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(
exchange, pairs=expanded_pairs, timeframes=config['timeframes'],
datadir=config['datadir'], timerange=timerange,

View File

@ -64,6 +64,7 @@ class Exchange:
"time_in_force_parameter": "timeInForce",
"ohlcv_params": {},
"ohlcv_candle_limit": 500,
"ohlcv_has_history": True, # Some exchanges (Kraken) don't provide history via ohlcv
"ohlcv_partial_candle": True,
"ohlcv_require_since": False,
# Check https://github.com/ccxt/ccxt/issues/10767 for removal of ohlcv_volume_currency
@ -308,12 +309,15 @@ class Exchange:
if self.log_responses:
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
Uses ohlcv_candle_limit_per_timeframe if the exchange has different limits
per timeframe (e.g. bittrex), otherwise falls back to ohlcv_candle_limit
:param timeframe: Timeframe to check
:param candle_type: Candle-type
:param since_ms: Starting timestamp
:return: Candle limit as integer
"""
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().
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.
candle_count = startup_candles + 1
# Allow 5 calls to the exchange per pair
required_candle_call_count = int(
(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:
# Only allow 5 calls per pair to somewhat limit the impact
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}.")
if required_candle_call_count > 1:
logger.warning(f"Using {required_candle_call_count} calls to get OHLCV. "
f"This can result in slower operations for the bot. Please check "
@ -1444,6 +1457,23 @@ class Exchange:
except ccxt.BaseError as 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,
side: EntryExit, is_short: bool) -> float:
"""
@ -1470,20 +1500,7 @@ class Exchange:
conf_strategy = self._config.get(strat_name, {})
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)]
price_side = self._get_price_side(side, is_short, conf_strategy)
price_side_word = price_side.capitalize()
@ -1703,7 +1720,8 @@ class Exchange:
: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(
"one_call: %s msecs (%s)",
one_call,
@ -1739,7 +1757,8 @@ class Exchange:
if (not since_ms
and (self._ft_has["ohlcv_require_since"] or self.required_candle_call_count > 1)):
# 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
now = timeframe_to_next_date(timeframe)
since_ms = int((now - timedelta(seconds=move_to // 1000)).timestamp() * 1000)
@ -1857,7 +1876,9 @@ class Exchange:
pair, timeframe, since_ms, s
)
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:
params.update({'price': candle_type})
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:
"""
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 date: date to use. Defaults to utcnow()
:param date: date to use. Defaults to now(utc)
:returns: date of previous candle (with utc timezone)
"""
if not date:
@ -2691,7 +2713,7 @@ def timeframe_to_next_date(timeframe: str, date: datetime = None) -> datetime:
"""
Use Timeframe and determine next candle.
: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)
"""
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)
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:
"""
Return True if the market is active.

View File

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

View File

@ -1,13 +1,15 @@
import logging
from typing import Dict, List, Tuple
from typing import Dict, List, Optional, Tuple
import ccxt
from freqtrade.constants import BuySell
from freqtrade.enums import MarginMode, TradingMode
from freqtrade.enums.candletype import CandleType
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
from freqtrade.exchange import Exchange
from freqtrade.exchange.common import retrier
from freqtrade.exchange.exchange import date_minus_candles
logger = logging.getLogger(__name__)
@ -20,7 +22,7 @@ class Okx(Exchange):
"""
_ft_has: Dict = {
"ohlcv_candle_limit": 100,
"ohlcv_candle_limit": 100, # Warning, special case with data prior to X months
"mark_ohlcv_timeframe": "4h",
"funding_fee_timeframe": "8h",
}
@ -37,6 +39,27 @@ class Okx(Exchange):
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
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:
# 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:
# We should decrease our position
@ -586,6 +587,7 @@ class FreqtradeBot(LoggingMixin):
ordertype: Optional[str] = None,
enter_tag: Optional[str] = None,
trade: Optional[Trade] = None,
order_adjust: bool = False
) -> bool:
"""
Executes a limit buy for the given pair
@ -601,7 +603,7 @@ class FreqtradeBot(LoggingMixin):
pos_adjust = trade is not None
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:
return False
@ -746,23 +748,26 @@ class FreqtradeBot(LoggingMixin):
self, pair: str, price: Optional[float], stake_amount: float,
trade_side: LongShort,
entry_tag: Optional[str],
trade: Optional[Trade]
trade: Optional[Trade],
order_adjust: bool,
) -> Tuple[float, float, float]:
if price:
enter_limit_requested = price
else:
# 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)
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,
default_retval=proposed_enter_rate)(
default_retval=enter_limit_requested)(
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,
)
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:
raise PricingError('Could not determine entry price.')
@ -1212,7 +1217,8 @@ class FreqtradeBot(LoggingMixin):
stake_amount=(order_obj.remaining * order_obj.price),
price=adjusted_entry_price,
trade=trade,
is_short=trade.is_short
is_short=trade.is_short,
order_adjust=True,
)
def cancel_all_open_orders(self) -> None:
@ -1408,6 +1414,7 @@ class FreqtradeBot(LoggingMixin):
open_date=trade.open_date_utc,
)
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):
exit_type = 'stoploss'
@ -1425,7 +1432,7 @@ class FreqtradeBot(LoggingMixin):
pair=trade.pair, trade=trade,
current_time=datetime.now(timezone.utc),
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)
@ -1442,8 +1449,8 @@ class FreqtradeBot(LoggingMixin):
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,
time_in_force=time_in_force, exit_reason=exit_check.exit_reason,
sell_reason=exit_check.exit_reason, # sellreason -> compatibility
time_in_force=time_in_force, exit_reason=exit_reason,
sell_reason=exit_reason, # sellreason -> compatibility
current_time=datetime.now(timezone.utc)):
logger.info(f"User requested abortion of exiting {trade.pair}")
return False
@ -1472,7 +1479,7 @@ class FreqtradeBot(LoggingMixin):
trade.open_order_id = order['id']
trade.exit_order_status = ''
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
self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),

View File

@ -535,6 +535,7 @@ class Backtesting:
if exit_.exit_flag:
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)
try:
@ -545,6 +546,15 @@ class Backtesting:
current_profit = trade.calc_profit_ratio(close_rate)
order_type = self.strategy.order_types['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
if order_type == 'limit':
close_rate = strategy_safe_wrapper(self.strategy.custom_exit_price,
@ -552,7 +562,7 @@ class Backtesting:
pair=trade.pair, trade=trade,
current_time=exit_candle_time,
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.
# freqtrade does not support this in live, and the order would fill immediately
if trade.is_short:
@ -566,22 +576,12 @@ class Backtesting:
pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount,
rate=close_rate,
time_in_force=time_in_force,
sell_reason=exit_.exit_reason, # deprecated
exit_reason=exit_.exit_reason,
sell_reason=exit_reason, # deprecated
exit_reason=exit_reason,
current_time=exit_candle_time):
return None
trade.exit_reason = exit_.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]
trade.exit_reason = exit_reason
self.order_id_counter += 1
order = Order(
@ -812,11 +812,11 @@ class Backtesting:
remaining=amount,
cost=stake_amount + trade.fee_open,
)
trade.orders.append(order)
if pos_adjust and self._get_order_filled(order.price, row):
order.close_bt_order(current_time, trade)
else:
trade.open_order_id = str(self.order_id_counter)
trade.orders.append(order)
trade.recalc_trade_from_orders()
return trade

View File

@ -153,6 +153,7 @@ class Order(_DECL_BASE):
and len(trade.select_filled_orders(trade.entry_side)) == 1):
trade.open_rate = self.price
trade.recalc_open_trade_value()
trade.adjust_stop_loss(trade.open_rate, trade.stop_loss_pct, refresh=True)
@staticmethod
def update_orders(orders: List['Order'], order: Dict[str, Any]):
@ -491,7 +492,7 @@ class LocalTrade():
self.stoploss_last_update = datetime.utcnow()
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
: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):
# Don't modify if called with initial and nothing to do
return
refresh = True if refresh and self.nr_of_successful_entries == 1 else False
leverage = self.leverage or 1.0
if self.is_short:
@ -516,8 +518,7 @@ class LocalTrade():
new_loss = max(self.liquidation_price, new_loss)
# no stop loss assigned yet
if self.initial_stop_loss_pct is None:
logger.debug(f"{self.pair} - Assigning new stoploss...")
if self.initial_stop_loss_pct is None or refresh:
self._set_stop_loss(new_loss, stoploss)
self.initial_stop_loss = new_loss
self.initial_stop_loss_pct = -1 * abs(stoploss)
@ -656,7 +657,7 @@ class LocalTrade():
def recalc_open_trade_value(self) -> None:
"""
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()

View File

@ -32,18 +32,19 @@ class AgeFilter(IPairList):
self._min_days_listed = pairlistconfig.get('min_days_listed', 10)
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:
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 "
"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:
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 "
"exchange max request size "
f"({exchange.ohlcv_candle_limit('1d')})")
f"({candle_limit})")
@property
def needstickers(self) -> bool:

View File

@ -19,6 +19,7 @@ class OffsetFilter(IPairList):
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
self._offset = pairlistconfig.get('offset', 0)
self._number_pairs = pairlistconfig.get('number_assets', 0)
if self._offset < 0:
raise OperationalException("OffsetFilter requires offset to be >= 0")
@ -36,7 +37,9 @@ class OffsetFilter(IPairList):
"""
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]:
"""
@ -50,5 +53,9 @@ class OffsetFilter(IPairList):
self.log_once(f"Offset of {self._offset} is larger than " +
f"pair count of {len(pairlist)}", logger.warning)
pairs = pairlist[self._offset:]
if self._number_pairs:
pairs = pairs[:self._number_pairs]
self.log_once(f"Searching {len(pairs)} pairs: {pairs}", logger.info)
return pairs

View File

@ -38,12 +38,12 @@ class VolatilityFilter(IPairList):
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:
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 "
"exceed exchange max request size "
f"({exchange.ohlcv_candle_limit('1d')})")
f"exceed exchange max request size ({candle_limit})")
@property
def needstickers(self) -> bool:

View File

@ -84,12 +84,13 @@ class VolumePairList(IPairList):
raise OperationalException(
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:
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 "
"exceed exchange max request size "
f"({exchange.ohlcv_candle_limit(self._lookback_timeframe)})")
f"exceed exchange max request size ({candle_limit})")
@property
def needstickers(self) -> bool:

View File

@ -33,12 +33,12 @@ class RangeStabilityFilter(IPairList):
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:
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 "
"exceed exchange max request size "
f"({exchange.ohlcv_candle_limit('1d')})")
f"exceed exchange max request size ({candle_limit})")
@property
def needstickers(self) -> bool:

View File

@ -1417,7 +1417,7 @@ class Telegram(RPCHandler):
"*/stopbuy:* `Stops buying, but handles open trades gracefully` \n"
"*/forceexit <trade_id>|all:* `Instantly exits the given trade or all trades, "
"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 ''}"
"*/delete <trade_id>:* `Instantly delete the given trade in the database`\n"
"*/whitelist:* `Show current whitelist` \n"

View File

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

View File

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

View File

@ -1,4 +1,4 @@
# Include all requirements to run the bot.
-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-ta==0.3.14b
ccxt==1.81.81
ccxt==1.82.61
# Pin cryptography for now due to rust build errors with piwheels
cryptography==37.0.2
aiohttp==3.8.1
@ -34,9 +34,9 @@ orjson==3.6.8
sdnotify==0.3.2
# API Server
fastapi==0.76.0
fastapi==0.78.0
uvicorn==0.17.6
pyjwt==2.3.0
pyjwt==2.4.0
aiofiles==0.8.0
psutil==5.9.0

View File

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

View File

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

View File

@ -835,6 +835,23 @@ def test_download_data_trades(mocker, caplog):
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):
convert_mock = mocker.patch('freqtrade.commands.data_commands.convert_trades_to_ohlcv',
MagicMock(return_value=[]))

View File

@ -13,6 +13,7 @@ import pytest
from freqtrade.enums import CandleType
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 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
def test_fetch_ohlcv(self, exchange):
def test_ccxt_fetch_ohlcv(self, exchange):
exchange, exchangename = exchange
pair = EXCHANGES[exchangename]['pair']
timeframe = EXCHANGES[exchangename]['timeframe']
@ -231,11 +232,44 @@ class TestCCXTExchange():
assert len(ohlcv[pair_tf]) == len(exchange.klines(pair_tf))
# assert len(exchange.klines(pair_tf)) > 200
# 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
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)
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):
exchange, exchangename = exchange_futures
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.common import (API_FETCH_ORDER_RETRY_COUNT, API_RETRY_COUNT,
calculate_backoff, remove_credentials)
from freqtrade.exchange.exchange import (market_is_active, timeframe_to_minutes, timeframe_to_msecs,
timeframe_to_next_date, timeframe_to_prev_date,
timeframe_to_seconds)
from freqtrade.exchange.exchange import (date_minus_candles, market_is_active, timeframe_to_minutes,
timeframe_to_msecs, timeframe_to_next_date,
timeframe_to_prev_date, timeframe_to_seconds)
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
@ -939,6 +939,7 @@ def test_validate_timeframes_emulated_ohlcvi_2(default_conf, mocker):
def test_validate_timeframes_not_in_config(default_conf, mocker):
# TODO: this test does not assert ...
del default_conf["timeframe"]
api_mock = MagicMock()
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_stakecurrency')
mocker.patch('freqtrade.exchange.Exchange.validate_pricing')
mocker.patch('freqtrade.exchange.Exchange.validate_required_startup_candles')
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.*'):
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):
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)
# 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(
pair,
"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)
# 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(
pair,
"5m",
@ -1995,7 +2004,7 @@ async def test__async_get_historic_ohlcv(default_conf, mocker, caplog, exchange_
)
# Required candles
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.
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]
# This should only run for 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():
@ -3424,6 +3433,17 @@ def test_timeframe_to_next_date():
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(
"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
import pytest
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.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(
default_conf,
mocker,

View File

@ -762,7 +762,7 @@ tc48 = BTContainer(data=[
[2, 4900, 5250, 4500, 5100, 6172, 0, 0], # Order readjust
[3, 5100, 5100, 4650, 4750, 6172, 0, 1],
[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,
custom_entry_price=4200, adjust_entry_price=5200,
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
[3, 5100, 5100, 4650, 4750, 6172, 0, 0, 0, 1],
[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,
custom_entry_price=5300, adjust_entry_price=5000,
trades=[BTrade(exit_reason=ExitType.EXIT_SIGNAL, open_tick=1, close_tick=4, is_short=True)]
@ -811,6 +811,35 @@ tc51 = BTContainer(data=[
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 = [
tc0,
tc1,
@ -864,6 +893,8 @@ TESTS = [
tc49,
tc50,
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.close_date == _get_frame_time_from_offset(trade.close_tick)
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']),
# VolumePairList with no offset = unchanged pairlist
([{"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']),
# VolumePairList with offset = 2
([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"},
{"method": "OffsetFilter", "offset": 2}],
"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
([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"},
{"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.'}]",
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,
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()
assert len(Trade.get_trades().all()) == 1
trade = Trade.get_trades().first()
trade: Trade = Trade.get_trades().first()
assert len(trade.orders) == 1
assert trade.open_order_id is not None
assert pytest.approx(trade.stake_amount) == 60
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
freqtrade.process()
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
# Open rate is not adjusted yet
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
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
# Open rate is not adjusted yet
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
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=120)