Merge branch 'develop' into keep_dataframe_noapi
This commit is contained in:
@@ -22,7 +22,8 @@ ORDERBOOK_SIDES = ['ask', 'bid']
|
||||
ORDERTYPE_POSSIBILITIES = ['limit', 'market']
|
||||
ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc']
|
||||
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList',
|
||||
'PrecisionFilter', 'PriceFilter', 'ShuffleFilter', 'SpreadFilter']
|
||||
'AgeFilter', 'PrecisionFilter', 'PriceFilter',
|
||||
'ShuffleFilter', 'SpreadFilter']
|
||||
AVAILABLE_DATAHANDLERS = ['json', 'jsongz']
|
||||
DRY_RUN_WALLET = 1000
|
||||
MATH_CLOSE_PREC = 1e-14 # Precision used for float comparisons
|
||||
@@ -221,6 +222,8 @@ CONF_SCHEMA = {
|
||||
},
|
||||
'username': {'type': 'string'},
|
||||
'password': {'type': 'string'},
|
||||
'jwt_secret_key': {'type': 'string'},
|
||||
'CORS_origins': {'type': 'array', 'items': {'type': 'string'}},
|
||||
'verbosity': {'type': 'string', 'enum': ['error', 'info']},
|
||||
},
|
||||
'required': ['enabled', 'listen_ip_address', 'listen_port', 'username', 'password']
|
||||
|
@@ -13,7 +13,7 @@ from pandas import DataFrame
|
||||
|
||||
from freqtrade.constants import ListPairsWithTimeframes, PairWithTimeframe
|
||||
from freqtrade.data.history import load_pair_history
|
||||
from freqtrade.exceptions import DependencyException, OperationalException
|
||||
from freqtrade.exceptions import ExchangeError, OperationalException
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.state import RunMode
|
||||
|
||||
@@ -132,7 +132,7 @@ class DataProvider:
|
||||
"""
|
||||
try:
|
||||
return self._exchange.fetch_ticker(pair)
|
||||
except DependencyException:
|
||||
except ExchangeError:
|
||||
return {}
|
||||
|
||||
def orderbook(self, pair: str, maximum: int) -> Dict[str, List]:
|
||||
|
@@ -270,6 +270,11 @@ def _download_trades_history(exchange: Exchange,
|
||||
# DEFAULT_TRADES_COLUMNS: 0 -> timestamp
|
||||
# DEFAULT_TRADES_COLUMNS: 1 -> id
|
||||
|
||||
if trades and since < trades[0][0]:
|
||||
# since is before the first trade
|
||||
logger.info(f"Start earlier than available data. Redownloading trades for {pair}...")
|
||||
trades = []
|
||||
|
||||
from_id = trades[-1][1] if trades else None
|
||||
if trades and since < trades[-1][0]:
|
||||
# Reset since to the last available point
|
||||
|
@@ -37,7 +37,21 @@ class InvalidOrderException(FreqtradeException):
|
||||
"""
|
||||
|
||||
|
||||
class TemporaryError(FreqtradeException):
|
||||
class RetryableOrderError(InvalidOrderException):
|
||||
"""
|
||||
This is returned when the order is not found.
|
||||
This Error will be repeated with increasing backof (in line with DDosError).
|
||||
"""
|
||||
|
||||
|
||||
class ExchangeError(DependencyException):
|
||||
"""
|
||||
Error raised out of the exchange.
|
||||
Has multiple Errors to determine the appropriate error.
|
||||
"""
|
||||
|
||||
|
||||
class TemporaryError(ExchangeError):
|
||||
"""
|
||||
Temporary network or exchange related error.
|
||||
This could happen when an exchange is congested, unavailable, or the user
|
||||
@@ -45,6 +59,13 @@ class TemporaryError(FreqtradeException):
|
||||
"""
|
||||
|
||||
|
||||
class DDosProtection(TemporaryError):
|
||||
"""
|
||||
Temporary error caused by DDOS protection.
|
||||
Bot will wait for a second and then retry.
|
||||
"""
|
||||
|
||||
|
||||
class StrategyError(FreqtradeException):
|
||||
"""
|
||||
Errors with custom user-code deteced.
|
||||
|
@@ -4,9 +4,11 @@ from typing import Dict
|
||||
|
||||
import ccxt
|
||||
|
||||
from freqtrade.exceptions import (DependencyException, InvalidOrderException,
|
||||
OperationalException, TemporaryError)
|
||||
from freqtrade.exceptions import (DDosProtection, ExchangeError,
|
||||
InvalidOrderException, OperationalException,
|
||||
TemporaryError)
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.common import retrier
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -39,6 +41,7 @@ class Binance(Exchange):
|
||||
"""
|
||||
return order['type'] == 'stop_loss_limit' and stop_loss > float(order['info']['stopPrice'])
|
||||
|
||||
@retrier(retries=0)
|
||||
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict:
|
||||
"""
|
||||
creates a stoploss limit order.
|
||||
@@ -77,7 +80,7 @@ class Binance(Exchange):
|
||||
'stop price: %s. limit: %s', pair, stop_price, rate)
|
||||
return order
|
||||
except ccxt.InsufficientFunds as e:
|
||||
raise DependencyException(
|
||||
raise ExchangeError(
|
||||
f'Insufficient funds to create {ordertype} sell order on market {pair}.'
|
||||
f'Tried to sell amount {amount} at rate {rate}. '
|
||||
f'Message: {e}') from e
|
||||
@@ -88,6 +91,8 @@ class Binance(Exchange):
|
||||
f'Could not create {ordertype} sell order on market {pair}. '
|
||||
f'Tried to sell amount {amount} at rate {rate}. '
|
||||
f'Message: {e}') from e
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e
|
||||
|
@@ -1,6 +1,10 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
from functools import wraps
|
||||
|
||||
from freqtrade.exceptions import TemporaryError
|
||||
from freqtrade.exceptions import (DDosProtection, RetryableOrderError,
|
||||
TemporaryError)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -88,6 +92,13 @@ MAP_EXCHANGE_CHILDCLASS = {
|
||||
}
|
||||
|
||||
|
||||
def calculate_backoff(retrycount, max_retries):
|
||||
"""
|
||||
Calculate backoff
|
||||
"""
|
||||
return (max_retries - retrycount) ** 2 + 1
|
||||
|
||||
|
||||
def retrier_async(f):
|
||||
async def wrapper(*args, **kwargs):
|
||||
count = kwargs.pop('count', API_RETRY_COUNT)
|
||||
@@ -99,6 +110,10 @@ def retrier_async(f):
|
||||
count -= 1
|
||||
kwargs.update({'count': count})
|
||||
logger.warning('retrying %s() still for %s times', f.__name__, count)
|
||||
if isinstance(ex, DDosProtection):
|
||||
backoff_delay = calculate_backoff(count + 1, API_RETRY_COUNT)
|
||||
logger.debug(f"Applying DDosProtection backoff delay: {backoff_delay}")
|
||||
await asyncio.sleep(backoff_delay)
|
||||
return await wrapper(*args, **kwargs)
|
||||
else:
|
||||
logger.warning('Giving up retrying: %s()', f.__name__)
|
||||
@@ -106,19 +121,31 @@ def retrier_async(f):
|
||||
return wrapper
|
||||
|
||||
|
||||
def retrier(f):
|
||||
def wrapper(*args, **kwargs):
|
||||
count = kwargs.pop('count', API_RETRY_COUNT)
|
||||
try:
|
||||
return f(*args, **kwargs)
|
||||
except TemporaryError as ex:
|
||||
logger.warning('%s() returned exception: "%s"', f.__name__, ex)
|
||||
if count > 0:
|
||||
count -= 1
|
||||
kwargs.update({'count': count})
|
||||
logger.warning('retrying %s() still for %s times', f.__name__, count)
|
||||
return wrapper(*args, **kwargs)
|
||||
else:
|
||||
logger.warning('Giving up retrying: %s()', f.__name__)
|
||||
raise ex
|
||||
return wrapper
|
||||
def retrier(_func=None, retries=API_RETRY_COUNT):
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def wrapper(*args, **kwargs):
|
||||
count = kwargs.pop('count', retries)
|
||||
try:
|
||||
return f(*args, **kwargs)
|
||||
except (TemporaryError, RetryableOrderError) as ex:
|
||||
logger.warning('%s() returned exception: "%s"', f.__name__, ex)
|
||||
if count > 0:
|
||||
count -= 1
|
||||
kwargs.update({'count': count})
|
||||
logger.warning('retrying %s() still for %s times', f.__name__, count)
|
||||
if isinstance(ex, DDosProtection) or isinstance(ex, RetryableOrderError):
|
||||
# increasing backoff
|
||||
backoff_delay = calculate_backoff(count + 1, retries)
|
||||
logger.debug(f"Applying DDosProtection backoff delay: {backoff_delay}")
|
||||
time.sleep(backoff_delay)
|
||||
return wrapper(*args, **kwargs)
|
||||
else:
|
||||
logger.warning('Giving up retrying: %s()', f.__name__)
|
||||
raise ex
|
||||
return wrapper
|
||||
# Support both @retrier and @retrier(retries=2) syntax
|
||||
if _func is None:
|
||||
return decorator
|
||||
else:
|
||||
return decorator(_func)
|
||||
|
@@ -18,12 +18,13 @@ from ccxt.base.decimal_to_precision import (ROUND_DOWN, ROUND_UP, TICK_SIZE,
|
||||
TRUNCATE, decimal_to_precision)
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.constants import ListPairsWithTimeframes
|
||||
from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list
|
||||
from freqtrade.exceptions import (DependencyException, InvalidOrderException,
|
||||
OperationalException, TemporaryError)
|
||||
from freqtrade.exceptions import (DDosProtection, ExchangeError,
|
||||
InvalidOrderException, OperationalException,
|
||||
RetryableOrderError, TemporaryError)
|
||||
from freqtrade.exchange.common import BAD_EXCHANGES, retrier, retrier_async
|
||||
from freqtrade.misc import deep_merge_dicts, safe_value_fallback
|
||||
from freqtrade.constants import ListPairsWithTimeframes
|
||||
|
||||
CcxtModuleType = Any
|
||||
|
||||
@@ -351,7 +352,7 @@ class Exchange:
|
||||
for pair in [f"{curr_1}/{curr_2}", f"{curr_2}/{curr_1}"]:
|
||||
if pair in self.markets and self.markets[pair].get('active'):
|
||||
return pair
|
||||
raise DependencyException(f"Could not combine {curr_1} and {curr_2} to get a valid pair.")
|
||||
raise ExchangeError(f"Could not combine {curr_1} and {curr_2} to get a valid pair.")
|
||||
|
||||
def validate_timeframes(self, timeframe: Optional[str]) -> None:
|
||||
"""
|
||||
@@ -518,15 +519,17 @@ class Exchange:
|
||||
amount, rate_for_order, params)
|
||||
|
||||
except ccxt.InsufficientFunds as e:
|
||||
raise DependencyException(
|
||||
raise ExchangeError(
|
||||
f'Insufficient funds to create {ordertype} {side} order on market {pair}.'
|
||||
f'Tried to {side} amount {amount} at rate {rate}.'
|
||||
f'Message: {e}') from e
|
||||
except ccxt.InvalidOrder as e:
|
||||
raise DependencyException(
|
||||
raise ExchangeError(
|
||||
f'Could not create {ordertype} {side} order on market {pair}.'
|
||||
f'Tried to {side} amount {amount} at rate {rate}.'
|
||||
f'Message: {e}') from e
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not place {side} order due to {e.__class__.__name__}. Message: {e}') from e
|
||||
@@ -606,6 +609,8 @@ class Exchange:
|
||||
balances.pop("used", None)
|
||||
|
||||
return balances
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not get balance due to {e.__class__.__name__}. Message: {e}') from e
|
||||
@@ -620,6 +625,8 @@ class Exchange:
|
||||
raise OperationalException(
|
||||
f'Exchange {self._api.name} does not support fetching tickers in batch. '
|
||||
f'Message: {e}') from e
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not load tickers due to {e.__class__.__name__}. Message: {e}') from e
|
||||
@@ -630,9 +637,11 @@ class Exchange:
|
||||
def fetch_ticker(self, pair: str) -> dict:
|
||||
try:
|
||||
if pair not in self._api.markets or not self._api.markets[pair].get('active'):
|
||||
raise DependencyException(f"Pair {pair} not available")
|
||||
raise ExchangeError(f"Pair {pair} not available")
|
||||
data = self._api.fetch_ticker(pair)
|
||||
return data
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not load ticker due to {e.__class__.__name__}. Message: {e}') from e
|
||||
@@ -766,6 +775,8 @@ class Exchange:
|
||||
raise OperationalException(
|
||||
f'Exchange {self._api.name} does not support fetching historical '
|
||||
f'candle (OHLCV) data. Message: {e}') from e
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(f'Could not fetch historical candle (OHLCV) data '
|
||||
f'for pair {pair} due to {e.__class__.__name__}. '
|
||||
@@ -802,6 +813,8 @@ class Exchange:
|
||||
raise OperationalException(
|
||||
f'Exchange {self._api.name} does not support fetching historical trade data.'
|
||||
f'Message: {e}') from e
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(f'Could not load trade history due to {e.__class__.__name__}. '
|
||||
f'Message: {e}') from e
|
||||
@@ -933,7 +946,7 @@ class Exchange:
|
||||
def check_order_canceled_empty(self, order: Dict) -> bool:
|
||||
"""
|
||||
Verify if an order has been cancelled without being partially filled
|
||||
:param order: Order dict as returned from get_order()
|
||||
:param order: Order dict as returned from fetch_order()
|
||||
:return: True if order has been cancelled without being filled, False otherwise.
|
||||
"""
|
||||
return order.get('status') in ('closed', 'canceled') and order.get('filled') == 0.0
|
||||
@@ -948,13 +961,15 @@ class Exchange:
|
||||
except ccxt.InvalidOrder as e:
|
||||
raise InvalidOrderException(
|
||||
f'Could not cancel order. Message: {e}') from e
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not cancel order due to {e.__class__.__name__}. Message: {e}') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
# Assign method to get_stoploss_order to allow easy overriding in other classes
|
||||
# Assign method to fetch_stoploss_order to allow easy overriding in other classes
|
||||
cancel_stoploss_order = cancel_order
|
||||
|
||||
def is_cancel_order_result_suitable(self, corder) -> bool:
|
||||
@@ -968,7 +983,7 @@ class Exchange:
|
||||
"""
|
||||
Cancel order returning a result.
|
||||
Creates a fake result if cancel order returns a non-usable result
|
||||
and get_order does not work (certain exchanges don't return cancelled orders)
|
||||
and fetch_order does not work (certain exchanges don't return cancelled orders)
|
||||
:param order_id: Orderid to cancel
|
||||
:param pair: Pair corresponding to order_id
|
||||
:param amount: Amount to use for fake response
|
||||
@@ -981,7 +996,7 @@ class Exchange:
|
||||
except InvalidOrderException:
|
||||
logger.warning(f"Could not cancel order {order_id}.")
|
||||
try:
|
||||
order = self.get_order(order_id, pair)
|
||||
order = self.fetch_order(order_id, pair)
|
||||
except InvalidOrderException:
|
||||
logger.warning(f"Could not fetch cancelled order {order_id}.")
|
||||
order = {'fee': {}, 'status': 'canceled', 'amount': amount, 'info': {}}
|
||||
@@ -989,7 +1004,7 @@ class Exchange:
|
||||
return order
|
||||
|
||||
@retrier
|
||||
def get_order(self, order_id: str, pair: str) -> Dict:
|
||||
def fetch_order(self, order_id: str, pair: str) -> Dict:
|
||||
if self._config['dry_run']:
|
||||
try:
|
||||
order = self._dry_run_open_orders[order_id]
|
||||
@@ -1000,17 +1015,22 @@ class Exchange:
|
||||
f'Tried to get an invalid dry-run-order (id: {order_id}). Message: {e}') from e
|
||||
try:
|
||||
return self._api.fetch_order(order_id, pair)
|
||||
except ccxt.OrderNotFound as e:
|
||||
raise RetryableOrderError(
|
||||
f'Order not found (id: {order_id}). Message: {e}') from e
|
||||
except ccxt.InvalidOrder as e:
|
||||
raise InvalidOrderException(
|
||||
f'Tried to get an invalid order (id: {order_id}). Message: {e}') from e
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not get order due to {e.__class__.__name__}. Message: {e}') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
# Assign method to get_stoploss_order to allow easy overriding in other classes
|
||||
get_stoploss_order = get_order
|
||||
# Assign method to fetch_stoploss_order to allow easy overriding in other classes
|
||||
fetch_stoploss_order = fetch_order
|
||||
|
||||
@retrier
|
||||
def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict:
|
||||
@@ -1027,6 +1047,8 @@ class Exchange:
|
||||
raise OperationalException(
|
||||
f'Exchange {self._api.name} does not support fetching order book.'
|
||||
f'Message: {e}') from e
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not get order book due to {e.__class__.__name__}. Message: {e}') from e
|
||||
@@ -1063,7 +1085,8 @@ class Exchange:
|
||||
matched_trades = [trade for trade in my_trades if trade['order'] == order_id]
|
||||
|
||||
return matched_trades
|
||||
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not get trades due to {e.__class__.__name__}. Message: {e}') from e
|
||||
@@ -1080,6 +1103,8 @@ class Exchange:
|
||||
|
||||
return self._api.calculate_fee(symbol=symbol, type=type, side=side, amount=amount,
|
||||
price=price, takerOrMaker=taker_or_maker)['rate']
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not get fee info due to {e.__class__.__name__}. Message: {e}') from e
|
||||
@@ -1129,7 +1154,7 @@ class Exchange:
|
||||
|
||||
fee_to_quote_rate = safe_value_fallback(tick, tick, 'last', 'ask')
|
||||
return round((order['fee']['cost'] * fee_to_quote_rate) / order['cost'], 8)
|
||||
except DependencyException:
|
||||
except ExchangeError:
|
||||
return None
|
||||
|
||||
def extract_cost_curr_rate(self, order: Dict) -> Tuple[float, str, Optional[float]]:
|
||||
|
@@ -4,8 +4,9 @@ from typing import Dict
|
||||
|
||||
import ccxt
|
||||
|
||||
from freqtrade.exceptions import (DependencyException, InvalidOrderException,
|
||||
OperationalException, TemporaryError)
|
||||
from freqtrade.exceptions import (DDosProtection, ExchangeError,
|
||||
InvalidOrderException, OperationalException,
|
||||
TemporaryError)
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.common import retrier
|
||||
|
||||
@@ -26,6 +27,7 @@ class Ftx(Exchange):
|
||||
"""
|
||||
return order['type'] == 'stop' and stop_loss > float(order['price'])
|
||||
|
||||
@retrier(retries=0)
|
||||
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict:
|
||||
"""
|
||||
Creates a stoploss order.
|
||||
@@ -59,7 +61,7 @@ class Ftx(Exchange):
|
||||
'stop price: %s.', pair, stop_price)
|
||||
return order
|
||||
except ccxt.InsufficientFunds as e:
|
||||
raise DependencyException(
|
||||
raise ExchangeError(
|
||||
f'Insufficient funds to create {ordertype} sell order on market {pair}. '
|
||||
f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
|
||||
f'Message: {e}') from e
|
||||
@@ -68,6 +70,8 @@ class Ftx(Exchange):
|
||||
f'Could not create {ordertype} sell order on market {pair}. '
|
||||
f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
|
||||
f'Message: {e}') from e
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e
|
||||
@@ -75,7 +79,7 @@ class Ftx(Exchange):
|
||||
raise OperationalException(e) from e
|
||||
|
||||
@retrier
|
||||
def get_stoploss_order(self, order_id: str, pair: str) -> Dict:
|
||||
def fetch_stoploss_order(self, order_id: str, pair: str) -> Dict:
|
||||
if self._config['dry_run']:
|
||||
try:
|
||||
order = self._dry_run_open_orders[order_id]
|
||||
@@ -96,6 +100,8 @@ class Ftx(Exchange):
|
||||
except ccxt.InvalidOrder as e:
|
||||
raise InvalidOrderException(
|
||||
f'Tried to get an invalid order (id: {order_id}). Message: {e}') from e
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not get order due to {e.__class__.__name__}. Message: {e}') from e
|
||||
@@ -111,6 +117,8 @@ class Ftx(Exchange):
|
||||
except ccxt.InvalidOrder as e:
|
||||
raise InvalidOrderException(
|
||||
f'Could not cancel order. Message: {e}') from e
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not cancel order due to {e.__class__.__name__}. Message: {e}') from e
|
||||
|
@@ -4,8 +4,9 @@ from typing import Dict
|
||||
|
||||
import ccxt
|
||||
|
||||
from freqtrade.exceptions import (DependencyException, InvalidOrderException,
|
||||
OperationalException, TemporaryError)
|
||||
from freqtrade.exceptions import (DDosProtection, ExchangeError,
|
||||
InvalidOrderException, OperationalException,
|
||||
TemporaryError)
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.common import retrier
|
||||
|
||||
@@ -45,6 +46,8 @@ class Kraken(Exchange):
|
||||
balances[bal]['free'] = balances[bal]['total'] - balances[bal]['used']
|
||||
|
||||
return balances
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not get balance due to {e.__class__.__name__}. Message: {e}') from e
|
||||
@@ -58,6 +61,7 @@ class Kraken(Exchange):
|
||||
"""
|
||||
return order['type'] == 'stop-loss' and stop_loss > float(order['price'])
|
||||
|
||||
@retrier(retries=0)
|
||||
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict:
|
||||
"""
|
||||
Creates a stoploss market order.
|
||||
@@ -84,7 +88,7 @@ class Kraken(Exchange):
|
||||
'stop price: %s.', pair, stop_price)
|
||||
return order
|
||||
except ccxt.InsufficientFunds as e:
|
||||
raise DependencyException(
|
||||
raise ExchangeError(
|
||||
f'Insufficient funds to create {ordertype} sell order on market {pair}.'
|
||||
f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
|
||||
f'Message: {e}') from e
|
||||
@@ -93,6 +97,8 @@ class Kraken(Exchange):
|
||||
f'Could not create {ordertype} sell order on market {pair}. '
|
||||
f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
|
||||
f'Message: {e}') from e
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e
|
||||
|
@@ -11,14 +11,14 @@ from typing import Any, Dict, List, Optional
|
||||
|
||||
import arrow
|
||||
from cachetools import TTLCache
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
from freqtrade import __version__, constants, persistence
|
||||
from freqtrade.configuration import validate_config_consistency
|
||||
from freqtrade.data.converter import order_book_to_dataframe
|
||||
from freqtrade.data.dataprovider import DataProvider
|
||||
from freqtrade.edge import Edge
|
||||
from freqtrade.exceptions import DependencyException, InvalidOrderException, PricingError
|
||||
from freqtrade.exceptions import (DependencyException, ExchangeError,
|
||||
InvalidOrderException, PricingError)
|
||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date
|
||||
from freqtrade.misc import safe_value_fallback
|
||||
from freqtrade.pairlist.pairlistmanager import PairListManager
|
||||
@@ -765,7 +765,7 @@ class FreqtradeBot:
|
||||
logger.warning('Selling the trade forcefully')
|
||||
self.execute_sell(trade, trade.stop_loss, sell_reason=SellType.EMERGENCY_SELL)
|
||||
|
||||
except DependencyException:
|
||||
except ExchangeError:
|
||||
trade.stoploss_order_id = None
|
||||
logger.exception('Unable to place a stoploss order on exchange.')
|
||||
return False
|
||||
@@ -783,8 +783,8 @@ class FreqtradeBot:
|
||||
|
||||
try:
|
||||
# First we check if there is already a stoploss on exchange
|
||||
stoploss_order = self.exchange.get_stoploss_order(trade.stoploss_order_id, trade.pair) \
|
||||
if trade.stoploss_order_id else None
|
||||
stoploss_order = self.exchange.fetch_stoploss_order(
|
||||
trade.stoploss_order_id, trade.pair) if trade.stoploss_order_id else None
|
||||
except InvalidOrderException as exception:
|
||||
logger.warning('Unable to fetch stoploss order: %s', exception)
|
||||
|
||||
@@ -900,8 +900,8 @@ class FreqtradeBot:
|
||||
try:
|
||||
if not trade.open_order_id:
|
||||
continue
|
||||
order = self.exchange.get_order(trade.open_order_id, trade.pair)
|
||||
except (RequestException, DependencyException, InvalidOrderException):
|
||||
order = self.exchange.fetch_order(trade.open_order_id, trade.pair)
|
||||
except (ExchangeError, InvalidOrderException):
|
||||
logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc())
|
||||
continue
|
||||
|
||||
@@ -933,7 +933,7 @@ class FreqtradeBot:
|
||||
|
||||
for trade in Trade.get_open_order_trades():
|
||||
try:
|
||||
order = self.exchange.get_order(trade.open_order_id, trade.pair)
|
||||
order = self.exchange.fetch_order(trade.open_order_id, trade.pair)
|
||||
except (DependencyException, InvalidOrderException):
|
||||
logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc())
|
||||
continue
|
||||
@@ -1220,7 +1220,7 @@ class FreqtradeBot:
|
||||
# Update trade with order values
|
||||
logger.info('Found open order for %s', trade)
|
||||
try:
|
||||
order = action_order or self.exchange.get_order(order_id, trade.pair)
|
||||
order = action_order or self.exchange.fetch_order(order_id, trade.pair)
|
||||
except InvalidOrderException as exception:
|
||||
logger.warning('Unable to fetch order %s: %s', order_id, exception)
|
||||
return False
|
||||
|
@@ -65,20 +65,6 @@ class Backtesting:
|
||||
self.strategylist: List[IStrategy] = []
|
||||
self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config)
|
||||
|
||||
self.pairlists = PairListManager(self.exchange, self.config)
|
||||
if 'VolumePairList' in self.pairlists.name_list:
|
||||
raise OperationalException("VolumePairList not allowed for backtesting.")
|
||||
|
||||
self.pairlists.refresh_pairlist()
|
||||
|
||||
if len(self.pairlists.whitelist) == 0:
|
||||
raise OperationalException("No pair in whitelist.")
|
||||
|
||||
if config.get('fee'):
|
||||
self.fee = config['fee']
|
||||
else:
|
||||
self.fee = self.exchange.get_fee(symbol=self.pairlists.whitelist[0])
|
||||
|
||||
if self.config.get('runmode') != RunMode.HYPEROPT:
|
||||
self.dataprovider = DataProvider(self.config, self.exchange)
|
||||
IStrategy.dp = self.dataprovider
|
||||
@@ -101,6 +87,25 @@ class Backtesting:
|
||||
self.timeframe = str(self.config.get('timeframe'))
|
||||
self.timeframe_min = timeframe_to_minutes(self.timeframe)
|
||||
|
||||
self.pairlists = PairListManager(self.exchange, self.config)
|
||||
if 'VolumePairList' in self.pairlists.name_list:
|
||||
raise OperationalException("VolumePairList not allowed for backtesting.")
|
||||
|
||||
if len(self.strategylist) > 1 and 'PrecisionFilter' in self.pairlists.name_list:
|
||||
raise OperationalException(
|
||||
"PrecisionFilter not allowed for backtesting multiple strategies."
|
||||
)
|
||||
|
||||
self.pairlists.refresh_pairlist()
|
||||
|
||||
if len(self.pairlists.whitelist) == 0:
|
||||
raise OperationalException("No pair in whitelist.")
|
||||
|
||||
if config.get('fee'):
|
||||
self.fee = config['fee']
|
||||
else:
|
||||
self.fee = self.exchange.get_fee(symbol=self.pairlists.whitelist[0])
|
||||
|
||||
# Get maximum required startup period
|
||||
self.required_startup = max([strat.startup_candle_count for strat in self.strategylist])
|
||||
# Load one (first) strategy
|
||||
|
76
freqtrade/pairlist/AgeFilter.py
Normal file
76
freqtrade/pairlist/AgeFilter.py
Normal file
@@ -0,0 +1,76 @@
|
||||
"""
|
||||
Minimum age (days listed) pair list filter
|
||||
"""
|
||||
import logging
|
||||
import arrow
|
||||
from typing import Any, Dict
|
||||
|
||||
from freqtrade.misc import plural
|
||||
from freqtrade.pairlist.IPairList import IPairList
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AgeFilter(IPairList):
|
||||
|
||||
# Checked symbols cache (dictionary of ticker symbol => timestamp)
|
||||
_symbolsChecked: Dict[str, int] = {}
|
||||
|
||||
def __init__(self, exchange, pairlistmanager,
|
||||
config: Dict[str, Any], pairlistconfig: Dict[str, Any],
|
||||
pairlist_pos: int) -> None:
|
||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||
|
||||
self._min_days_listed = pairlistconfig.get('min_days_listed', 10)
|
||||
self._enabled = self._min_days_listed >= 1
|
||||
|
||||
@property
|
||||
def needstickers(self) -> bool:
|
||||
"""
|
||||
Boolean property defining if tickers are necessary.
|
||||
If no Pairlist requires tickers, an empty List is passed
|
||||
as tickers argument to filter_pairlist
|
||||
"""
|
||||
return True
|
||||
|
||||
def short_desc(self) -> str:
|
||||
"""
|
||||
Short whitelist method description - used for startup-messages
|
||||
"""
|
||||
return (f"{self.name} - Filtering pairs with age less than "
|
||||
f"{self._min_days_listed} {plural(self._min_days_listed, 'day')}.")
|
||||
|
||||
def _validate_pair(self, ticker: dict) -> bool:
|
||||
"""
|
||||
Validate age for the ticker
|
||||
:param ticker: ticker dict as returned from ccxt.load_markets()
|
||||
:return: True if the pair can stay, False if it should be removed
|
||||
"""
|
||||
|
||||
# Check symbol in cache
|
||||
if ticker['symbol'] in self._symbolsChecked:
|
||||
return True
|
||||
|
||||
since_ms = int(arrow.utcnow()
|
||||
.floor('day')
|
||||
.shift(days=-self._min_days_listed)
|
||||
.float_timestamp) * 1000
|
||||
|
||||
daily_candles = self._exchange.get_historic_ohlcv(pair=ticker['symbol'],
|
||||
timeframe='1d',
|
||||
since_ms=since_ms)
|
||||
|
||||
if daily_candles is not None:
|
||||
if len(daily_candles) > self._min_days_listed:
|
||||
# We have fetched at least the minimum required number of daily candles
|
||||
# Add to cache, store the time we last checked this symbol
|
||||
self._symbolsChecked[ticker['symbol']] = int(arrow.utcnow().float_timestamp) * 1000
|
||||
return True
|
||||
else:
|
||||
self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, "
|
||||
f"because age is less than "
|
||||
f"{self._min_days_listed} "
|
||||
f"{plural(self._min_days_listed, 'day')}")
|
||||
return False
|
||||
return False
|
@@ -68,7 +68,7 @@ class IPairList(ABC):
|
||||
def needstickers(self) -> bool:
|
||||
"""
|
||||
Boolean property defining if tickers are necessary.
|
||||
If no Pairlist requries tickers, an empty List is passed
|
||||
If no Pairlist requires tickers, an empty List is passed
|
||||
as tickers argument to filter_pairlist
|
||||
"""
|
||||
|
||||
|
@@ -5,7 +5,7 @@ import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
from freqtrade.pairlist.IPairList import IPairList
|
||||
|
||||
from freqtrade.exceptions import OperationalException
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -17,6 +17,10 @@ class PrecisionFilter(IPairList):
|
||||
pairlist_pos: int) -> None:
|
||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||
|
||||
if 'stoploss' not in self._config:
|
||||
raise OperationalException(
|
||||
'PrecisionFilter can only work with stoploss defined. Please add the '
|
||||
'stoploss key to your configuration (overwrites eventual strategy settings).')
|
||||
self._stoploss = self._config['stoploss']
|
||||
self._enabled = self._stoploss != 0
|
||||
|
||||
@@ -27,7 +31,7 @@ class PrecisionFilter(IPairList):
|
||||
def needstickers(self) -> bool:
|
||||
"""
|
||||
Boolean property defining if tickers are necessary.
|
||||
If no Pairlist requries tickers, an empty List is passed
|
||||
If no Pairlist requires tickers, an empty List is passed
|
||||
as tickers argument to filter_pairlist
|
||||
"""
|
||||
return True
|
||||
|
@@ -24,7 +24,7 @@ class PriceFilter(IPairList):
|
||||
def needstickers(self) -> bool:
|
||||
"""
|
||||
Boolean property defining if tickers are necessary.
|
||||
If no Pairlist requries tickers, an empty List is passed
|
||||
If no Pairlist requires tickers, an empty List is passed
|
||||
as tickers argument to filter_pairlist
|
||||
"""
|
||||
return True
|
||||
|
@@ -25,7 +25,7 @@ class ShuffleFilter(IPairList):
|
||||
def needstickers(self) -> bool:
|
||||
"""
|
||||
Boolean property defining if tickers are necessary.
|
||||
If no Pairlist requries tickers, an empty List is passed
|
||||
If no Pairlist requires tickers, an empty List is passed
|
||||
as tickers argument to filter_pairlist
|
||||
"""
|
||||
return False
|
||||
|
@@ -24,7 +24,7 @@ class SpreadFilter(IPairList):
|
||||
def needstickers(self) -> bool:
|
||||
"""
|
||||
Boolean property defining if tickers are necessary.
|
||||
If no Pairlist requries tickers, an empty List is passed
|
||||
If no Pairlist requires tickers, an empty List is passed
|
||||
as tickers argument to filter_pairlist
|
||||
"""
|
||||
return True
|
||||
|
@@ -28,7 +28,7 @@ class StaticPairList(IPairList):
|
||||
def needstickers(self) -> bool:
|
||||
"""
|
||||
Boolean property defining if tickers are necessary.
|
||||
If no Pairlist requries tickers, an empty List is passed
|
||||
If no Pairlist requires tickers, an empty List is passed
|
||||
as tickers argument to filter_pairlist
|
||||
"""
|
||||
return False
|
||||
|
@@ -54,7 +54,7 @@ class VolumePairList(IPairList):
|
||||
def needstickers(self) -> bool:
|
||||
"""
|
||||
Boolean property defining if tickers are necessary.
|
||||
If no Pairlist requries tickers, an empty List is passed
|
||||
If no Pairlist requires tickers, an empty List is passed
|
||||
as tickers argument to filter_pairlist
|
||||
"""
|
||||
return True
|
||||
|
@@ -360,7 +360,7 @@ class Trade(_DECL_BASE):
|
||||
def update(self, order: Dict) -> None:
|
||||
"""
|
||||
Updates this entity with amount and actual open/close rates.
|
||||
:param order: order retrieved by exchange.get_order()
|
||||
:param order: order retrieved by exchange.fetch_order()
|
||||
:return: None
|
||||
"""
|
||||
order_type = order['type']
|
||||
|
@@ -90,7 +90,9 @@ class ApiServer(RPC):
|
||||
self._config = freqtrade.config
|
||||
self.app = Flask(__name__)
|
||||
self._cors = CORS(self.app,
|
||||
resources={r"/api/*": {"supports_credentials": True, }}
|
||||
resources={r"/api/*": {
|
||||
"supports_credentials": True,
|
||||
"origins": self._config['api_server'].get('CORS_origins', [])}}
|
||||
)
|
||||
|
||||
# Setup the Flask-JWT-Extended extension
|
||||
|
@@ -11,7 +11,7 @@ from typing import Any, Dict, List, Optional, Tuple
|
||||
import arrow
|
||||
from numpy import NAN, mean
|
||||
|
||||
from freqtrade.exceptions import DependencyException, TemporaryError
|
||||
from freqtrade.exceptions import ExchangeError, PricingError
|
||||
from freqtrade.misc import shorten_date
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
|
||||
@@ -126,11 +126,11 @@ class RPC:
|
||||
for trade in trades:
|
||||
order = None
|
||||
if trade.open_order_id:
|
||||
order = self._freqtrade.exchange.get_order(trade.open_order_id, trade.pair)
|
||||
order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair)
|
||||
# calculate profit and send message to user
|
||||
try:
|
||||
current_rate = self._freqtrade.get_sell_rate(trade.pair, False)
|
||||
except DependencyException:
|
||||
except (ExchangeError, PricingError):
|
||||
current_rate = NAN
|
||||
current_profit = trade.calc_profit_ratio(current_rate)
|
||||
current_profit_abs = trade.calc_profit(current_rate)
|
||||
@@ -174,7 +174,7 @@ class RPC:
|
||||
# calculate profit and send message to user
|
||||
try:
|
||||
current_rate = self._freqtrade.get_sell_rate(trade.pair, False)
|
||||
except DependencyException:
|
||||
except (PricingError, ExchangeError):
|
||||
current_rate = NAN
|
||||
trade_percent = (100 * trade.calc_profit_ratio(current_rate))
|
||||
trade_profit = trade.calc_profit(current_rate)
|
||||
@@ -286,7 +286,7 @@ class RPC:
|
||||
# Get current rate
|
||||
try:
|
||||
current_rate = self._freqtrade.get_sell_rate(trade.pair, False)
|
||||
except DependencyException:
|
||||
except (PricingError, ExchangeError):
|
||||
current_rate = NAN
|
||||
profit_ratio = trade.calc_profit_ratio(rate=current_rate)
|
||||
|
||||
@@ -352,7 +352,7 @@ class RPC:
|
||||
total = 0.0
|
||||
try:
|
||||
tickers = self._freqtrade.exchange.get_tickers()
|
||||
except (TemporaryError, DependencyException):
|
||||
except (ExchangeError):
|
||||
raise RPCException('Error getting current tickers.')
|
||||
|
||||
self._freqtrade.wallets.update(require_update=False)
|
||||
@@ -373,7 +373,7 @@ class RPC:
|
||||
if pair.startswith(stake_currency):
|
||||
rate = 1.0 / rate
|
||||
est_stake = rate * balance.total
|
||||
except (TemporaryError, DependencyException):
|
||||
except (ExchangeError):
|
||||
logger.warning(f" Could not get rate for pair {coin}.")
|
||||
continue
|
||||
total = total + (est_stake or 0)
|
||||
@@ -442,7 +442,7 @@ class RPC:
|
||||
def _exec_forcesell(trade: Trade) -> None:
|
||||
# Check if there is there is an open order
|
||||
if trade.open_order_id:
|
||||
order = self._freqtrade.exchange.get_order(trade.open_order_id, trade.pair)
|
||||
order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair)
|
||||
|
||||
# Cancel open LIMIT_BUY orders and close trade
|
||||
if order and order['status'] == 'open' \
|
||||
|
@@ -59,6 +59,7 @@
|
||||
"listen_port": 8080,
|
||||
"verbosity": "info",
|
||||
"jwt_secret_key": "somethingrandom",
|
||||
"CORS_origins": [],
|
||||
"username": "",
|
||||
"password": ""
|
||||
},
|
||||
|
Reference in New Issue
Block a user