merged with feat/short
This commit is contained in:
@@ -53,7 +53,7 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None:
|
||||
|
||||
if epochs and export_csv:
|
||||
HyperoptTools.export_csv_file(
|
||||
config, epochs, total_epochs, not config.get('hyperopt_list_best', False), export_csv
|
||||
config, epochs, export_csv
|
||||
)
|
||||
|
||||
|
||||
|
19
freqtrade/configuration/PeriodicCache.py
Normal file
19
freqtrade/configuration/PeriodicCache.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from cachetools.ttl import TTLCache
|
||||
|
||||
|
||||
class PeriodicCache(TTLCache):
|
||||
"""
|
||||
Special cache that expires at "straight" times
|
||||
A timer with ttl of 3600 (1h) will expire at every full hour (:00).
|
||||
"""
|
||||
|
||||
def __init__(self, maxsize, ttl, getsizeof=None):
|
||||
def local_timer():
|
||||
ts = datetime.now(timezone.utc).timestamp()
|
||||
offset = (ts % ttl)
|
||||
return ts - offset
|
||||
|
||||
# Init with smlight offset
|
||||
super().__init__(maxsize=maxsize, ttl=ttl-1e-5, timer=local_timer, getsizeof=getsizeof)
|
@@ -4,4 +4,5 @@ from freqtrade.configuration.check_exchange import check_exchange
|
||||
from freqtrade.configuration.config_setup import setup_utils_configuration
|
||||
from freqtrade.configuration.config_validation import validate_config_consistency
|
||||
from freqtrade.configuration.configuration import Configuration
|
||||
from freqtrade.configuration.PeriodicCache import PeriodicCache
|
||||
from freqtrade.configuration.timerange import TimeRange
|
||||
|
@@ -119,7 +119,7 @@ class Edge:
|
||||
)
|
||||
# Download informative pairs too
|
||||
res = defaultdict(list)
|
||||
for p, t in self.strategy.informative_pairs():
|
||||
for p, t in self.strategy.gather_informative_pairs():
|
||||
res[t].append(p)
|
||||
for timeframe, inf_pairs in res.items():
|
||||
timerange_startup = deepcopy(self._timerange)
|
||||
|
@@ -20,4 +20,7 @@ class Bibox(Exchange):
|
||||
|
||||
# fetchCurrencies API point requires authentication for Bibox,
|
||||
# so switch it off for Freqtrade load_markets()
|
||||
_ccxt_config: Dict = {"has": {"fetchCurrencies": False}}
|
||||
@property
|
||||
def _ccxt_config(self) -> Dict:
|
||||
# Parameters to add directly to ccxt sync/async initialization.
|
||||
return {"has": {"fetchCurrencies": False}}
|
||||
|
@@ -1,10 +1,13 @@
|
||||
""" Binance exchange subclass """
|
||||
import json
|
||||
import logging
|
||||
from typing import Dict, List
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
import arrow
|
||||
import ccxt
|
||||
|
||||
from freqtrade.enums import Collateral, TradingMode
|
||||
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
|
||||
OperationalException, TemporaryError)
|
||||
from freqtrade.exchange import Exchange
|
||||
@@ -27,36 +30,74 @@ class Binance(Exchange):
|
||||
}
|
||||
funding_fee_times: List[int] = [0, 8, 16] # hours of the day
|
||||
|
||||
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
|
||||
_supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [
|
||||
# TradingMode.SPOT always supported and not required in this list
|
||||
# (TradingMode.MARGIN, Collateral.CROSS), # TODO-lev: Uncomment once supported
|
||||
# (TradingMode.FUTURES, Collateral.CROSS), # TODO-lev: Uncomment once supported
|
||||
# (TradingMode.FUTURES, Collateral.ISOLATED) # TODO-lev: Uncomment once supported
|
||||
]
|
||||
|
||||
@property
|
||||
def _ccxt_config(self) -> Dict:
|
||||
# Parameters to add directly to ccxt sync/async initialization.
|
||||
if self.trading_mode == TradingMode.MARGIN:
|
||||
return {
|
||||
"options": {
|
||||
"defaultType": "margin"
|
||||
}
|
||||
}
|
||||
elif self.trading_mode == TradingMode.FUTURES:
|
||||
return {
|
||||
"options": {
|
||||
"defaultType": "future"
|
||||
}
|
||||
}
|
||||
else:
|
||||
return {}
|
||||
|
||||
def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool:
|
||||
"""
|
||||
Verify stop_loss against stoploss-order value (limit or price)
|
||||
Returns True if adjustment is necessary.
|
||||
:param side: "buy" or "sell"
|
||||
"""
|
||||
return order['type'] == 'stop_loss_limit' and stop_loss > float(order['info']['stopPrice'])
|
||||
|
||||
return order['type'] == 'stop_loss_limit' and (
|
||||
(side == "sell" and stop_loss > float(order['info']['stopPrice'])) or
|
||||
(side == "buy" and stop_loss < float(order['info']['stopPrice']))
|
||||
)
|
||||
|
||||
@retrier(retries=0)
|
||||
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict:
|
||||
def stoploss(self, pair: str, amount: float, stop_price: float,
|
||||
order_types: Dict, side: str, leverage: float) -> Dict:
|
||||
"""
|
||||
creates a stoploss limit order.
|
||||
this stoploss-limit is binance-specific.
|
||||
It may work with a limited number of other exchanges, but this has not been tested yet.
|
||||
:param side: "buy" or "sell"
|
||||
"""
|
||||
# Limit price threshold: As limit price should always be below stop-price
|
||||
limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
|
||||
rate = stop_price * limit_price_pct
|
||||
if side == "sell":
|
||||
# TODO: Name limit_rate in other exchange subclasses
|
||||
rate = stop_price * limit_price_pct
|
||||
else:
|
||||
rate = stop_price * (2 - limit_price_pct)
|
||||
|
||||
ordertype = "stop_loss_limit"
|
||||
|
||||
stop_price = self.price_to_precision(pair, stop_price)
|
||||
|
||||
bad_stop_price = (stop_price <= rate) if side == "sell" else (stop_price >= rate)
|
||||
|
||||
# Ensure rate is less than stop price
|
||||
if stop_price <= rate:
|
||||
if bad_stop_price:
|
||||
raise OperationalException(
|
||||
'In stoploss limit order, stop price should be more than limit price')
|
||||
'In stoploss limit order, stop price should be better than limit price')
|
||||
|
||||
if self._config['dry_run']:
|
||||
dry_order = self.create_dry_run_order(
|
||||
pair, ordertype, "sell", amount, stop_price)
|
||||
pair, ordertype, side, amount, stop_price, leverage)
|
||||
return dry_order
|
||||
|
||||
try:
|
||||
@@ -67,7 +108,8 @@ class Binance(Exchange):
|
||||
|
||||
rate = self.price_to_precision(pair, rate)
|
||||
|
||||
order = self._api.create_order(symbol=pair, type=ordertype, side='sell',
|
||||
self._lev_prep(pair, leverage)
|
||||
order = self._api.create_order(symbol=pair, type=ordertype, side=side,
|
||||
amount=amount, price=rate, params=params)
|
||||
logger.info('stoploss limit order added for %s. '
|
||||
'stop price: %s. limit: %s', pair, stop_price, rate)
|
||||
@@ -75,21 +117,96 @@ class Binance(Exchange):
|
||||
return order
|
||||
except ccxt.InsufficientFunds as e:
|
||||
raise InsufficientFundsError(
|
||||
f'Insufficient funds to create {ordertype} sell order on market {pair}. '
|
||||
f'Tried to sell amount {amount} at rate {rate}. '
|
||||
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:
|
||||
# Errors:
|
||||
# `binance Order would trigger immediately.`
|
||||
raise InvalidOrderException(
|
||||
f'Could not create {ordertype} sell order on market {pair}. '
|
||||
f'Tried to sell amount {amount} at rate {rate}. '
|
||||
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 sell order due to {e.__class__.__name__}. Message: {e}') from e
|
||||
f'Could not place {side} order due to {e.__class__.__name__}. Message: {e}') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
@retrier
|
||||
def fill_leverage_brackets(self):
|
||||
"""
|
||||
Assigns property _leverage_brackets to a dictionary of information about the leverage
|
||||
allowed on each pair
|
||||
"""
|
||||
if self.trading_mode == TradingMode.FUTURES:
|
||||
try:
|
||||
if self._config['dry_run']:
|
||||
leverage_brackets_path = (
|
||||
Path(__file__).parent / 'binance_leverage_brackets.json'
|
||||
)
|
||||
with open(leverage_brackets_path) as json_file:
|
||||
leverage_brackets = json.load(json_file)
|
||||
else:
|
||||
leverage_brackets = self._api.load_leverage_brackets()
|
||||
|
||||
for pair, brackets in leverage_brackets.items():
|
||||
self._leverage_brackets[pair] = [
|
||||
[
|
||||
min_amount,
|
||||
float(margin_req)
|
||||
] for [
|
||||
min_amount,
|
||||
margin_req
|
||||
] in brackets
|
||||
]
|
||||
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(f'Could not fetch leverage amounts due to'
|
||||
f'{e.__class__.__name__}. Message: {e}') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
def get_max_leverage(self, pair: Optional[str], nominal_value: Optional[float]) -> float:
|
||||
"""
|
||||
Returns the maximum leverage that a pair can be traded at
|
||||
:param pair: The base/quote currency pair being traded
|
||||
:nominal_value: The total value of the trade in quote currency (collateral + debt)
|
||||
"""
|
||||
pair_brackets = self._leverage_brackets[pair]
|
||||
max_lev = 1.0
|
||||
for [min_amount, margin_req] in pair_brackets:
|
||||
if nominal_value >= min_amount:
|
||||
max_lev = 1/margin_req
|
||||
return max_lev
|
||||
|
||||
@ retrier
|
||||
def _set_leverage(
|
||||
self,
|
||||
leverage: float,
|
||||
pair: Optional[str] = None,
|
||||
trading_mode: Optional[TradingMode] = None
|
||||
):
|
||||
"""
|
||||
Set's the leverage before making a trade, in order to not
|
||||
have the same leverage on every trade
|
||||
"""
|
||||
trading_mode = trading_mode or self.trading_mode
|
||||
|
||||
if self._config['dry_run'] or trading_mode != TradingMode.FUTURES:
|
||||
return
|
||||
|
||||
try:
|
||||
self._api.set_leverage(symbol=pair, leverage=leverage)
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
|
1214
freqtrade/exchange/binance_leverage_brackets.json
Normal file
1214
freqtrade/exchange/binance_leverage_brackets.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ import http
|
||||
import inspect
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from datetime import datetime, timezone
|
||||
from math import ceil
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
|
||||
@@ -22,6 +22,7 @@ from pandas import DataFrame
|
||||
from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES,
|
||||
ListPairsWithTimeframes)
|
||||
from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list
|
||||
from freqtrade.enums import Collateral, TradingMode
|
||||
from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError,
|
||||
InvalidOrderException, OperationalException, PricingError,
|
||||
RetryableOrderError, TemporaryError)
|
||||
@@ -48,9 +49,6 @@ class Exchange:
|
||||
|
||||
_config: Dict = {}
|
||||
|
||||
# Parameters to add directly to ccxt sync/async initialization.
|
||||
_ccxt_config: Dict = {}
|
||||
|
||||
# Parameters to add directly to buy/sell calls (like agreeing to trading agreement)
|
||||
_params: Dict = {}
|
||||
|
||||
@@ -75,6 +73,10 @@ class Exchange:
|
||||
_ft_has: Dict = {}
|
||||
funding_fee_times: List[int] = [] # hours of the day
|
||||
|
||||
_supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [
|
||||
# TradingMode.SPOT always supported and not required in this list
|
||||
]
|
||||
|
||||
def __init__(self, config: Dict[str, Any], validate: bool = True) -> None:
|
||||
"""
|
||||
Initializes this module with the given config,
|
||||
@@ -84,6 +86,7 @@ class Exchange:
|
||||
self._api: ccxt.Exchange = None
|
||||
self._api_async: ccxt_async.Exchange = None
|
||||
self._markets: Dict = {}
|
||||
self._leverage_brackets: Dict = {}
|
||||
|
||||
self._config.update(config)
|
||||
|
||||
@@ -126,14 +129,25 @@ class Exchange:
|
||||
self._trades_pagination = self._ft_has['trades_pagination']
|
||||
self._trades_pagination_arg = self._ft_has['trades_pagination_arg']
|
||||
|
||||
self.trading_mode: TradingMode = (
|
||||
TradingMode(config.get('trading_mode'))
|
||||
if config.get('trading_mode')
|
||||
else TradingMode.SPOT
|
||||
)
|
||||
self.collateral: Optional[Collateral] = (
|
||||
Collateral(config.get('collateral'))
|
||||
if config.get('collateral')
|
||||
else None
|
||||
)
|
||||
|
||||
# Initialize ccxt objects
|
||||
ccxt_config = self._ccxt_config.copy()
|
||||
ccxt_config = self._ccxt_config
|
||||
ccxt_config = deep_merge_dicts(exchange_config.get('ccxt_config', {}), ccxt_config)
|
||||
ccxt_config = deep_merge_dicts(exchange_config.get('ccxt_sync_config', {}), ccxt_config)
|
||||
|
||||
self._api = self._init_ccxt(exchange_config, ccxt_kwargs=ccxt_config)
|
||||
|
||||
ccxt_async_config = self._ccxt_config.copy()
|
||||
ccxt_async_config = self._ccxt_config
|
||||
ccxt_async_config = deep_merge_dicts(exchange_config.get('ccxt_config', {}),
|
||||
ccxt_async_config)
|
||||
ccxt_async_config = deep_merge_dicts(exchange_config.get('ccxt_async_config', {}),
|
||||
@@ -141,6 +155,9 @@ class Exchange:
|
||||
self._api_async = self._init_ccxt(
|
||||
exchange_config, ccxt_async, ccxt_kwargs=ccxt_async_config)
|
||||
|
||||
if self.trading_mode != TradingMode.SPOT:
|
||||
self.fill_leverage_brackets()
|
||||
|
||||
logger.info('Using Exchange "%s"', self.name)
|
||||
|
||||
if validate:
|
||||
@@ -158,7 +175,7 @@ class Exchange:
|
||||
self.validate_order_time_in_force(config.get('order_time_in_force', {}))
|
||||
self.validate_required_startup_candles(config.get('startup_candle_count', 0),
|
||||
config.get('timeframe', ''))
|
||||
|
||||
self.validate_trading_mode_and_collateral(self.trading_mode, self.collateral)
|
||||
# Converts the interval provided in minutes in config to seconds
|
||||
self.markets_refresh_interval: int = exchange_config.get(
|
||||
"markets_refresh_interval", 60) * 60
|
||||
@@ -211,6 +228,11 @@ class Exchange:
|
||||
|
||||
return api
|
||||
|
||||
@property
|
||||
def _ccxt_config(self) -> Dict:
|
||||
# Parameters to add directly to ccxt sync/async initialization.
|
||||
return {}
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""exchange Name (from ccxt)"""
|
||||
@@ -356,6 +378,7 @@ class Exchange:
|
||||
# Also reload async markets to avoid issues with newly listed pairs
|
||||
self._load_async_markets(reload=True)
|
||||
self._last_markets_refresh = arrow.utcnow().int_timestamp
|
||||
self.fill_leverage_brackets()
|
||||
except ccxt.BaseError:
|
||||
logger.exception("Could not reload markets.")
|
||||
|
||||
@@ -483,6 +506,25 @@ class Exchange:
|
||||
f"This strategy requires {startup_candles} candles to start. "
|
||||
f"{self.name} only provides {candle_limit} for {timeframe}.")
|
||||
|
||||
def validate_trading_mode_and_collateral(
|
||||
self,
|
||||
trading_mode: TradingMode,
|
||||
collateral: Optional[Collateral] # Only None when trading_mode = TradingMode.SPOT
|
||||
):
|
||||
"""
|
||||
Checks if freqtrade can perform trades using the configured
|
||||
trading mode(Margin, Futures) and Collateral(Cross, Isolated)
|
||||
Throws OperationalException:
|
||||
If the trading_mode/collateral type are not supported by freqtrade on this exchange
|
||||
"""
|
||||
if trading_mode != TradingMode.SPOT and (
|
||||
(trading_mode, collateral) not in self._supported_trading_mode_collateral_pairs
|
||||
):
|
||||
collateral_value = collateral and collateral.value
|
||||
raise OperationalException(
|
||||
f"Freqtrade does not support {collateral_value} {trading_mode.value} on {self.name}"
|
||||
)
|
||||
|
||||
def exchange_has(self, endpoint: str) -> bool:
|
||||
"""
|
||||
Checks if exchange implements a specific API endpoint.
|
||||
@@ -542,8 +584,8 @@ class Exchange:
|
||||
else:
|
||||
return 1 / pow(10, precision)
|
||||
|
||||
def get_min_pair_stake_amount(self, pair: str, price: float,
|
||||
stoploss: float) -> Optional[float]:
|
||||
def get_min_pair_stake_amount(self, pair: str, price: float, stoploss: float,
|
||||
leverage: Optional[float] = 1.0) -> Optional[float]:
|
||||
try:
|
||||
market = self.markets[pair]
|
||||
except KeyError:
|
||||
@@ -577,12 +619,24 @@ class Exchange:
|
||||
# The value returned should satisfy both limits: for amount (base currency) and
|
||||
# for cost (quote, stake currency), so max() is used here.
|
||||
# See also #2575 at github.
|
||||
return max(min_stake_amounts) * amount_reserve_percent
|
||||
return self._get_stake_amount_considering_leverage(
|
||||
max(min_stake_amounts) * amount_reserve_percent,
|
||||
leverage or 1.0
|
||||
)
|
||||
|
||||
def _get_stake_amount_considering_leverage(self, stake_amount: float, leverage: float):
|
||||
"""
|
||||
Takes the minimum stake amount for a pair with no leverage and returns the minimum
|
||||
stake amount when leverage is considered
|
||||
:param stake_amount: The stake amount for a pair before leverage is considered
|
||||
:param leverage: The amount of leverage being used on the current trade
|
||||
"""
|
||||
return stake_amount / leverage
|
||||
|
||||
# Dry-run methods
|
||||
|
||||
def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: float,
|
||||
rate: float, params: Dict = {}) -> Dict[str, Any]:
|
||||
rate: float, leverage: float, params: Dict = {}) -> Dict[str, Any]:
|
||||
order_id = f'dry_run_{side}_{datetime.now().timestamp()}'
|
||||
_amount = self.amount_to_precision(pair, amount)
|
||||
dry_order: Dict[str, Any] = {
|
||||
@@ -599,7 +653,8 @@ class Exchange:
|
||||
'timestamp': arrow.utcnow().int_timestamp * 1000,
|
||||
'status': "closed" if ordertype == "market" else "open",
|
||||
'fee': None,
|
||||
'info': {}
|
||||
'info': {},
|
||||
'leverage': leverage
|
||||
}
|
||||
if dry_order["type"] in ["stop_loss_limit", "stop-loss-limit"]:
|
||||
dry_order["info"] = {"stopPrice": dry_order["price"]}
|
||||
@@ -609,7 +664,7 @@ class Exchange:
|
||||
average = self.get_dry_market_fill_price(pair, side, amount, rate)
|
||||
dry_order.update({
|
||||
'average': average,
|
||||
'cost': dry_order['amount'] * average,
|
||||
'cost': (dry_order['amount'] * average) / leverage
|
||||
})
|
||||
dry_order = self.add_dry_order_fee(pair, dry_order)
|
||||
|
||||
@@ -717,17 +772,26 @@ class Exchange:
|
||||
|
||||
# Order handling
|
||||
|
||||
def create_order(self, pair: str, ordertype: str, side: str, amount: float,
|
||||
rate: float, time_in_force: str = 'gtc') -> Dict:
|
||||
|
||||
if self._config['dry_run']:
|
||||
dry_order = self.create_dry_run_order(pair, ordertype, side, amount, rate)
|
||||
return dry_order
|
||||
def _lev_prep(self, pair: str, leverage: float):
|
||||
if self.trading_mode != TradingMode.SPOT:
|
||||
self.set_margin_mode(pair, self.collateral)
|
||||
self._set_leverage(leverage, pair)
|
||||
|
||||
def _get_params(self, ordertype: str, leverage: float, time_in_force: str = 'gtc') -> Dict:
|
||||
params = self._params.copy()
|
||||
if time_in_force != 'gtc' and ordertype != 'market':
|
||||
param = self._ft_has.get('time_in_force_parameter', '')
|
||||
params.update({param: time_in_force})
|
||||
return params
|
||||
|
||||
def create_order(self, pair: str, ordertype: str, side: str, amount: float,
|
||||
rate: float, leverage: float = 1.0, time_in_force: str = 'gtc') -> Dict:
|
||||
# TODO-lev: remove default for leverage
|
||||
if self._config['dry_run']:
|
||||
dry_order = self.create_dry_run_order(pair, ordertype, side, amount, rate, leverage)
|
||||
return dry_order
|
||||
|
||||
params = self._get_params(ordertype, leverage, time_in_force)
|
||||
|
||||
try:
|
||||
# Set the precision for amount and price(rate) as accepted by the exchange
|
||||
@@ -736,6 +800,7 @@ class Exchange:
|
||||
or self._api.options.get("createMarketBuyOrderRequiresPrice", False))
|
||||
rate_for_order = self.price_to_precision(pair, rate) if needs_price else None
|
||||
|
||||
self._lev_prep(pair, leverage)
|
||||
order = self._api.create_order(pair, ordertype, side,
|
||||
amount, rate_for_order, params)
|
||||
self._log_exchange_response('create_order', order)
|
||||
@@ -759,14 +824,15 @@ class Exchange:
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
|
||||
def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool:
|
||||
"""
|
||||
Verify stop_loss against stoploss-order value (limit or price)
|
||||
Returns True if adjustment is necessary.
|
||||
"""
|
||||
raise OperationalException(f"stoploss is not implemented for {self.name}.")
|
||||
|
||||
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict:
|
||||
def stoploss(self, pair: str, amount: float, stop_price: float,
|
||||
order_types: Dict, side: str, leverage: float) -> Dict:
|
||||
"""
|
||||
creates a stoploss order.
|
||||
The precise ordertype is determined by the order_types dict or exchange default.
|
||||
@@ -1559,21 +1625,66 @@ class Exchange:
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
def _get_funding_fee_dates(self, open_date: datetime, close_date: datetime):
|
||||
def fill_leverage_brackets(self):
|
||||
"""
|
||||
Get's the date and time of every funding fee that happened between two datetimes
|
||||
# TODO-lev: Should maybe be renamed, leverage_brackets might not be accurate for kraken
|
||||
Assigns property _leverage_brackets to a dictionary of information about the leverage
|
||||
allowed on each pair
|
||||
"""
|
||||
open_date = datetime(open_date.year, open_date.month, open_date.day, open_date.hour)
|
||||
close_date = datetime(close_date.year, close_date.month, close_date.day, close_date.hour)
|
||||
return
|
||||
|
||||
results = []
|
||||
date_iterator = open_date
|
||||
while date_iterator < close_date:
|
||||
date_iterator += timedelta(hours=1)
|
||||
if date_iterator.hour in self.funding_fee_times:
|
||||
results.append(date_iterator)
|
||||
def get_max_leverage(self, pair: Optional[str], nominal_value: Optional[float]) -> float:
|
||||
"""
|
||||
Returns the maximum leverage that a pair can be traded at
|
||||
:param pair: The base/quote currency pair being traded
|
||||
:nominal_value: The total value of the trade in quote currency (collateral + debt)
|
||||
"""
|
||||
return 1.0
|
||||
|
||||
return results
|
||||
@retrier
|
||||
def _set_leverage(
|
||||
self,
|
||||
leverage: float,
|
||||
pair: Optional[str] = None,
|
||||
trading_mode: Optional[TradingMode] = None
|
||||
):
|
||||
"""
|
||||
Set's the leverage before making a trade, in order to not
|
||||
have the same leverage on every trade
|
||||
"""
|
||||
if self._config['dry_run'] or not self.exchange_has("setLeverage"):
|
||||
# Some exchanges only support one collateral type
|
||||
return
|
||||
|
||||
try:
|
||||
self._api.set_leverage(symbol=pair, leverage=leverage)
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
@retrier
|
||||
def set_margin_mode(self, pair: str, collateral: Collateral, params: dict = {}):
|
||||
'''
|
||||
Set's the margin mode on the exchange to cross or isolated for a specific pair
|
||||
:param symbol: base/quote currency pair (e.g. "ADA/USDT")
|
||||
'''
|
||||
if self._config['dry_run'] or not self.exchange_has("setMarginMode"):
|
||||
# Some exchanges only support one collateral type
|
||||
return
|
||||
|
||||
try:
|
||||
self._api.set_margin_mode(pair, collateral.value, params)
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not set margin mode due to {e.__class__.__name__}. Message: {e}') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
|
||||
def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool:
|
||||
|
@@ -1,9 +1,10 @@
|
||||
""" FTX exchange subclass """
|
||||
import logging
|
||||
from typing import Any, Dict, List
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import ccxt
|
||||
|
||||
from freqtrade.enums import Collateral, TradingMode
|
||||
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
|
||||
OperationalException, TemporaryError)
|
||||
from freqtrade.exchange import Exchange
|
||||
@@ -22,6 +23,12 @@ class Ftx(Exchange):
|
||||
}
|
||||
funding_fee_times: List[int] = list(range(0, 23))
|
||||
|
||||
_supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [
|
||||
# TradingMode.SPOT always supported and not required in this list
|
||||
# (TradingMode.MARGIN, Collateral.CROSS), # TODO-lev: Uncomment once supported
|
||||
# (TradingMode.FUTURES, Collateral.CROSS) # TODO-lev: Uncomment once supported
|
||||
]
|
||||
|
||||
def market_is_tradable(self, market: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
Check if the market symbol is tradable by Freqtrade.
|
||||
@@ -32,15 +39,19 @@ class Ftx(Exchange):
|
||||
return (parent_check and
|
||||
market.get('spot', False) is True)
|
||||
|
||||
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
|
||||
def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool:
|
||||
"""
|
||||
Verify stop_loss against stoploss-order value (limit or price)
|
||||
Returns True if adjustment is necessary.
|
||||
"""
|
||||
return order['type'] == 'stop' and stop_loss > float(order['price'])
|
||||
return order['type'] == 'stop' and (
|
||||
side == "sell" and stop_loss > float(order['price']) or
|
||||
side == "buy" and stop_loss < float(order['price'])
|
||||
)
|
||||
|
||||
@retrier(retries=0)
|
||||
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict:
|
||||
def stoploss(self, pair: str, amount: float, stop_price: float,
|
||||
order_types: Dict, side: str, leverage: float) -> Dict:
|
||||
"""
|
||||
Creates a stoploss order.
|
||||
depending on order_types.stoploss configuration, uses 'market' or limit order.
|
||||
@@ -48,7 +59,10 @@ class Ftx(Exchange):
|
||||
Limit orders are defined by having orderPrice set, otherwise a market order is used.
|
||||
"""
|
||||
limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
|
||||
limit_rate = stop_price * limit_price_pct
|
||||
if side == "sell":
|
||||
limit_rate = stop_price * limit_price_pct
|
||||
else:
|
||||
limit_rate = stop_price * (2 - limit_price_pct)
|
||||
|
||||
ordertype = "stop"
|
||||
|
||||
@@ -56,7 +70,7 @@ class Ftx(Exchange):
|
||||
|
||||
if self._config['dry_run']:
|
||||
dry_order = self.create_dry_run_order(
|
||||
pair, ordertype, "sell", amount, stop_price)
|
||||
pair, ordertype, side, amount, stop_price, leverage)
|
||||
return dry_order
|
||||
|
||||
try:
|
||||
@@ -68,7 +82,8 @@ class Ftx(Exchange):
|
||||
params['stopPrice'] = stop_price
|
||||
amount = self.amount_to_precision(pair, amount)
|
||||
|
||||
order = self._api.create_order(symbol=pair, type=ordertype, side='sell',
|
||||
self._lev_prep(pair, leverage)
|
||||
order = self._api.create_order(symbol=pair, type=ordertype, side=side,
|
||||
amount=amount, params=params)
|
||||
self._log_exchange_response('create_stoploss_order', order)
|
||||
logger.info('stoploss order added for %s. '
|
||||
@@ -76,19 +91,19 @@ class Ftx(Exchange):
|
||||
return order
|
||||
except ccxt.InsufficientFunds as e:
|
||||
raise InsufficientFundsError(
|
||||
f'Insufficient funds to create {ordertype} sell order on market {pair}. '
|
||||
f'Insufficient funds to create {ordertype} {side} order on market {pair}. '
|
||||
f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
|
||||
f'Message: {e}') from e
|
||||
except ccxt.InvalidOrder as e:
|
||||
raise InvalidOrderException(
|
||||
f'Could not create {ordertype} sell order on market {pair}. '
|
||||
f'Could not create {ordertype} {side} 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
|
||||
f'Could not place {side} order due to {e.__class__.__name__}. Message: {e}') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
@@ -153,3 +168,18 @@ class Ftx(Exchange):
|
||||
if order['type'] == 'stop':
|
||||
return safe_value_fallback2(order, order, 'id_stop', 'id')
|
||||
return order['id']
|
||||
|
||||
def fill_leverage_brackets(self):
|
||||
"""
|
||||
FTX leverage is static across the account, and doesn't change from pair to pair,
|
||||
so _leverage_brackets doesn't need to be set
|
||||
"""
|
||||
return
|
||||
|
||||
def get_max_leverage(self, pair: Optional[str], nominal_value: Optional[float]) -> float:
|
||||
"""
|
||||
Returns the maximum leverage that a pair can be traded at, which is always 20 on ftx
|
||||
:param pair: Here for super method, not used on FTX
|
||||
:nominal_value: Here for super method, not used on FTX
|
||||
"""
|
||||
return 20.0
|
||||
|
@@ -1,9 +1,10 @@
|
||||
""" Kraken exchange subclass """
|
||||
import logging
|
||||
from typing import Any, Dict, List
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import ccxt
|
||||
|
||||
from freqtrade.enums import Collateral, TradingMode
|
||||
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
|
||||
OperationalException, TemporaryError)
|
||||
from freqtrade.exchange import Exchange
|
||||
@@ -24,6 +25,12 @@ class Kraken(Exchange):
|
||||
}
|
||||
funding_fee_times: List[int] = [0, 4, 8, 12, 16, 20] # hours of the day
|
||||
|
||||
_supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [
|
||||
# TradingMode.SPOT always supported and not required in this list
|
||||
# (TradingMode.MARGIN, Collateral.CROSS), # TODO-lev: Uncomment once supported
|
||||
# (TradingMode.FUTURES, Collateral.CROSS) # TODO-lev: No CCXT support
|
||||
]
|
||||
|
||||
def market_is_tradable(self, market: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
Check if the market symbol is tradable by Freqtrade.
|
||||
@@ -68,16 +75,19 @@ class Kraken(Exchange):
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
|
||||
def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool:
|
||||
"""
|
||||
Verify stop_loss against stoploss-order value (limit or price)
|
||||
Returns True if adjustment is necessary.
|
||||
"""
|
||||
return (order['type'] in ('stop-loss', 'stop-loss-limit')
|
||||
and stop_loss > float(order['price']))
|
||||
return (order['type'] in ('stop-loss', 'stop-loss-limit') and (
|
||||
(side == "sell" and stop_loss > float(order['price'])) or
|
||||
(side == "buy" and stop_loss < float(order['price']))
|
||||
))
|
||||
|
||||
@retrier(retries=0)
|
||||
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict:
|
||||
def stoploss(self, pair: str, amount: float, stop_price: float,
|
||||
order_types: Dict, side: str, leverage: float) -> Dict:
|
||||
"""
|
||||
Creates a stoploss market order.
|
||||
Stoploss market orders is the only stoploss type supported by kraken.
|
||||
@@ -87,7 +97,10 @@ class Kraken(Exchange):
|
||||
if order_types.get('stoploss', 'market') == 'limit':
|
||||
ordertype = "stop-loss-limit"
|
||||
limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
|
||||
limit_rate = stop_price * limit_price_pct
|
||||
if side == "sell":
|
||||
limit_rate = stop_price * limit_price_pct
|
||||
else:
|
||||
limit_rate = stop_price * (2 - limit_price_pct)
|
||||
params['price2'] = self.price_to_precision(pair, limit_rate)
|
||||
else:
|
||||
ordertype = "stop-loss"
|
||||
@@ -96,13 +109,13 @@ class Kraken(Exchange):
|
||||
|
||||
if self._config['dry_run']:
|
||||
dry_order = self.create_dry_run_order(
|
||||
pair, ordertype, "sell", amount, stop_price)
|
||||
pair, ordertype, side, amount, stop_price, leverage)
|
||||
return dry_order
|
||||
|
||||
try:
|
||||
amount = self.amount_to_precision(pair, amount)
|
||||
|
||||
order = self._api.create_order(symbol=pair, type=ordertype, side='sell',
|
||||
order = self._api.create_order(symbol=pair, type=ordertype, side=side,
|
||||
amount=amount, price=stop_price, params=params)
|
||||
self._log_exchange_response('create_stoploss_order', order)
|
||||
logger.info('stoploss order added for %s. '
|
||||
@@ -110,18 +123,70 @@ class Kraken(Exchange):
|
||||
return order
|
||||
except ccxt.InsufficientFunds as e:
|
||||
raise InsufficientFundsError(
|
||||
f'Insufficient funds to create {ordertype} sell order on market {pair}. '
|
||||
f'Insufficient funds to create {ordertype} {side} order on market {pair}. '
|
||||
f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
|
||||
f'Message: {e}') from e
|
||||
except ccxt.InvalidOrder as e:
|
||||
raise InvalidOrderException(
|
||||
f'Could not create {ordertype} sell order on market {pair}. '
|
||||
f'Could not create {ordertype} {side} 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
|
||||
f'Could not place {side} order due to {e.__class__.__name__}. Message: {e}') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
def fill_leverage_brackets(self):
|
||||
"""
|
||||
Assigns property _leverage_brackets to a dictionary of information about the leverage
|
||||
allowed on each pair
|
||||
"""
|
||||
leverages = {}
|
||||
|
||||
for pair, market in self.markets.items():
|
||||
leverages[pair] = [1]
|
||||
info = market['info']
|
||||
leverage_buy = info.get('leverage_buy', [])
|
||||
leverage_sell = info.get('leverage_sell', [])
|
||||
if len(leverage_buy) > 0 or len(leverage_sell) > 0:
|
||||
if leverage_buy != leverage_sell:
|
||||
logger.warning(
|
||||
f"The buy({leverage_buy}) and sell({leverage_sell}) leverage are not equal"
|
||||
"for {pair}. Please notify freqtrade because this has never happened before"
|
||||
)
|
||||
if max(leverage_buy) <= max(leverage_sell):
|
||||
leverages[pair] += [int(lev) for lev in leverage_buy]
|
||||
else:
|
||||
leverages[pair] += [int(lev) for lev in leverage_sell]
|
||||
else:
|
||||
leverages[pair] += [int(lev) for lev in leverage_buy]
|
||||
self._leverage_brackets = leverages
|
||||
|
||||
def get_max_leverage(self, pair: Optional[str], nominal_value: Optional[float]) -> float:
|
||||
"""
|
||||
Returns the maximum leverage that a pair can be traded at
|
||||
:param pair: The base/quote currency pair being traded
|
||||
:nominal_value: Here for super class, not needed on Kraken
|
||||
"""
|
||||
return float(max(self._leverage_brackets[pair]))
|
||||
|
||||
def _set_leverage(
|
||||
self,
|
||||
leverage: float,
|
||||
pair: Optional[str] = None,
|
||||
trading_mode: Optional[TradingMode] = None
|
||||
):
|
||||
"""
|
||||
Kraken set's the leverage as an option in the order object, so we need to
|
||||
add it to params
|
||||
"""
|
||||
return
|
||||
|
||||
def _get_params(self, ordertype: str, leverage: float, time_in_force: str = 'gtc') -> Dict:
|
||||
params = super()._get_params(ordertype, leverage, time_in_force)
|
||||
if leverage > 1.0:
|
||||
params['leverage'] = leverage
|
||||
return params
|
||||
|
@@ -86,10 +86,10 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
self.dataprovider = DataProvider(self.config, self.exchange, self.pairlists)
|
||||
|
||||
# Attach Dataprovider to Strategy baseclass
|
||||
IStrategy.dp = self.dataprovider
|
||||
# Attach Wallets to Strategy baseclass
|
||||
IStrategy.wallets = self.wallets
|
||||
# Attach Dataprovider to strategy instance
|
||||
self.strategy.dp = self.dataprovider
|
||||
# Attach Wallets to strategy instance
|
||||
self.strategy.wallets = self.wallets
|
||||
|
||||
# Initializing Edge only if enabled
|
||||
self.edge = Edge(self.config, self.exchange, self.strategy) if \
|
||||
@@ -173,7 +173,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
# Refreshing candles
|
||||
self.dataprovider.refresh(self.pairlists.create_pair_list(self.active_pair_whitelist),
|
||||
self.strategy.informative_pairs())
|
||||
self.strategy.gather_informative_pairs())
|
||||
|
||||
strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)()
|
||||
|
||||
@@ -763,9 +763,14 @@ class FreqtradeBot(LoggingMixin):
|
||||
:return: True if the order succeeded, and False in case of problems.
|
||||
"""
|
||||
try:
|
||||
stoploss_order = self.exchange.stoploss(pair=trade.pair, amount=trade.amount,
|
||||
stop_price=stop_price,
|
||||
order_types=self.strategy.order_types)
|
||||
stoploss_order = self.exchange.stoploss(
|
||||
pair=trade.pair,
|
||||
amount=trade.amount,
|
||||
stop_price=stop_price,
|
||||
order_types=self.strategy.order_types,
|
||||
side=trade.exit_side,
|
||||
leverage=trade.leverage
|
||||
)
|
||||
|
||||
order_obj = Order.parse_from_ccxt_object(stoploss_order, trade.pair, 'stoploss')
|
||||
trade.orders.append(order_obj)
|
||||
@@ -857,11 +862,11 @@ class FreqtradeBot(LoggingMixin):
|
||||
# if trailing stoploss is enabled we check if stoploss value has changed
|
||||
# in which case we cancel stoploss order and put another one with new
|
||||
# value immediately
|
||||
self.handle_trailing_stoploss_on_exchange(trade, stoploss_order)
|
||||
self.handle_trailing_stoploss_on_exchange(trade, stoploss_order, side=trade.exit_side)
|
||||
|
||||
return False
|
||||
|
||||
def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: dict) -> None:
|
||||
def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: dict, side: str) -> None:
|
||||
"""
|
||||
Check to see if stoploss on exchange should be updated
|
||||
in case of trailing stoploss on exchange
|
||||
@@ -869,7 +874,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
:param order: Current on exchange stoploss order
|
||||
:return: None
|
||||
"""
|
||||
if self.exchange.stoploss_adjust(trade.stop_loss, order):
|
||||
if self.exchange.stoploss_adjust(trade.stop_loss, order, side):
|
||||
# we check if the update is necessary
|
||||
update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60)
|
||||
if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() >= update_beat:
|
||||
|
@@ -20,7 +20,7 @@ def interest(
|
||||
|
||||
:param exchange_name: The exchanged being trading on
|
||||
:param borrowed: The amount of currency being borrowed
|
||||
:param rate: The rate of interest
|
||||
:param rate: The rate of interest (i.e daily interest rate)
|
||||
:param hours: The time in hours that the currency has been borrowed for
|
||||
|
||||
Raises:
|
||||
@@ -36,7 +36,8 @@ def interest(
|
||||
# Rounded based on https://kraken-fees-calculator.github.io/
|
||||
return borrowed * rate * (one+ceil(hours/four))
|
||||
elif exchange_name == "ftx":
|
||||
# TODO-lev: Add FTX interest formula
|
||||
raise OperationalException(f"Leverage not available on {exchange_name} with freqtrade")
|
||||
# As Explained under #Interest rates section in
|
||||
# https://help.ftx.com/hc/en-us/articles/360053007671-Spot-Margin-Trading-Explainer
|
||||
return borrowed * rate * ceil(hours)/twenty_four
|
||||
else:
|
||||
raise OperationalException(f"Leverage not available on {exchange_name} with freqtrade")
|
||||
|
@@ -154,7 +154,7 @@ class Backtesting:
|
||||
self.strategy: IStrategy = strategy
|
||||
strategy.dp = self.dataprovider
|
||||
# Attach Wallets to Strategy baseclass
|
||||
IStrategy.wallets = self.wallets
|
||||
strategy.wallets = self.wallets
|
||||
# Set stoploss_on_exchange to false for backtesting,
|
||||
# since a "perfect" stoploss-sell is assumed anyway
|
||||
# And the regular "stoploss" function would not apply to that case
|
||||
|
@@ -8,6 +8,7 @@ from typing import Any, Dict
|
||||
|
||||
from freqtrade import constants
|
||||
from freqtrade.configuration import TimeRange, validate_config_consistency
|
||||
from freqtrade.data.dataprovider import DataProvider
|
||||
from freqtrade.edge import Edge
|
||||
from freqtrade.optimize.optimize_reports import generate_edge_table
|
||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||
@@ -33,6 +34,7 @@ class EdgeCli:
|
||||
self.config['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
|
||||
self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config)
|
||||
self.strategy = StrategyResolver.load_strategy(self.config)
|
||||
self.strategy.dp = DataProvider(config, None)
|
||||
|
||||
validate_config_consistency(self.config)
|
||||
|
||||
|
@@ -45,7 +45,7 @@ progressbar.streams.wrap_stdout()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
INITIAL_POINTS = 30
|
||||
INITIAL_POINTS = 5
|
||||
|
||||
# Keep no more than SKOPT_MODEL_QUEUE_SIZE models
|
||||
# in the skopt model queue, to optimize memory consumption
|
||||
@@ -241,7 +241,7 @@ class Hyperopt:
|
||||
|
||||
if HyperoptTools.has_space(self.config, 'buy'):
|
||||
logger.debug("Hyperopt has 'buy' space")
|
||||
self.buy_space = self.custom_hyperopt.indicator_space()
|
||||
self.buy_space = self.custom_hyperopt.buy_indicator_space()
|
||||
|
||||
if HyperoptTools.has_space(self.config, 'sell'):
|
||||
logger.debug("Hyperopt has 'sell' space")
|
||||
@@ -365,10 +365,20 @@ class Hyperopt:
|
||||
}
|
||||
|
||||
def get_optimizer(self, dimensions: List[Dimension], cpu_count) -> Optimizer:
|
||||
estimator = self.custom_hyperopt.generate_estimator()
|
||||
|
||||
acq_optimizer = "sampling"
|
||||
if isinstance(estimator, str):
|
||||
if estimator not in ("GP", "RF", "ET", "GBRT"):
|
||||
raise OperationalException(f"Estimator {estimator} not supported.")
|
||||
else:
|
||||
acq_optimizer = "auto"
|
||||
|
||||
logger.info(f"Using estimator {estimator}.")
|
||||
return Optimizer(
|
||||
dimensions,
|
||||
base_estimator="ET",
|
||||
acq_optimizer="auto",
|
||||
base_estimator=estimator,
|
||||
acq_optimizer=acq_optimizer,
|
||||
n_initial_points=INITIAL_POINTS,
|
||||
acq_optimizer_kwargs={'n_jobs': cpu_count},
|
||||
random_state=self.random_state,
|
||||
|
@@ -12,7 +12,7 @@ from freqtrade.exceptions import OperationalException
|
||||
with suppress(ImportError):
|
||||
from skopt.space import Dimension
|
||||
|
||||
from freqtrade.optimize.hyperopt_interface import IHyperOpt
|
||||
from freqtrade.optimize.hyperopt_interface import EstimatorType, IHyperOpt
|
||||
|
||||
|
||||
def _format_exception_message(space: str) -> str:
|
||||
@@ -56,7 +56,7 @@ class HyperOptAuto(IHyperOpt):
|
||||
else:
|
||||
_format_exception_message(category)
|
||||
|
||||
def indicator_space(self) -> List['Dimension']:
|
||||
def buy_indicator_space(self) -> List['Dimension']:
|
||||
return self._get_indicator_space('buy')
|
||||
|
||||
def sell_indicator_space(self) -> List['Dimension']:
|
||||
@@ -79,3 +79,6 @@ class HyperOptAuto(IHyperOpt):
|
||||
|
||||
def trailing_space(self) -> List['Dimension']:
|
||||
return self._get_func('trailing_space')()
|
||||
|
||||
def generate_estimator(self) -> EstimatorType:
|
||||
return self._get_func('generate_estimator')()
|
||||
|
@@ -5,8 +5,9 @@ This module defines the interface to apply for hyperopt
|
||||
import logging
|
||||
import math
|
||||
from abc import ABC
|
||||
from typing import Dict, List
|
||||
from typing import Dict, List, Union
|
||||
|
||||
from sklearn.base import RegressorMixin
|
||||
from skopt.space import Categorical, Dimension, Integer
|
||||
|
||||
from freqtrade.exchange import timeframe_to_minutes
|
||||
@@ -17,6 +18,8 @@ from freqtrade.strategy import IStrategy
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
EstimatorType = Union[RegressorMixin, str]
|
||||
|
||||
|
||||
class IHyperOpt(ABC):
|
||||
"""
|
||||
@@ -37,6 +40,14 @@ class IHyperOpt(ABC):
|
||||
IHyperOpt.ticker_interval = str(config['timeframe']) # DEPRECATED
|
||||
IHyperOpt.timeframe = str(config['timeframe'])
|
||||
|
||||
def generate_estimator(self) -> EstimatorType:
|
||||
"""
|
||||
Return base_estimator.
|
||||
Can be any of "GP", "RF", "ET", "GBRT" or an instance of a class
|
||||
inheriting from RegressorMixin (from sklearn).
|
||||
"""
|
||||
return 'ET'
|
||||
|
||||
def generate_roi_table(self, params: Dict) -> Dict[int, float]:
|
||||
"""
|
||||
Create a ROI table.
|
||||
|
@@ -7,6 +7,7 @@ from pathlib import Path
|
||||
from typing import Any, Dict, Iterator, List, Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import rapidjson
|
||||
import tabulate
|
||||
from colorama import Fore, Style
|
||||
@@ -298,8 +299,8 @@ class HyperoptTools():
|
||||
f"Objective: {results['loss']:.5f}")
|
||||
|
||||
@staticmethod
|
||||
def prepare_trials_columns(trials, legacy_mode: bool, has_drawdown: bool) -> str:
|
||||
|
||||
def prepare_trials_columns(trials: pd.DataFrame, legacy_mode: bool,
|
||||
has_drawdown: bool) -> pd.DataFrame:
|
||||
trials['Best'] = ''
|
||||
|
||||
if 'results_metrics.winsdrawslosses' not in trials.columns:
|
||||
@@ -435,8 +436,7 @@ class HyperoptTools():
|
||||
return table
|
||||
|
||||
@staticmethod
|
||||
def export_csv_file(config: dict, results: list, total_epochs: int, highlight_best: bool,
|
||||
csv_file: str) -> None:
|
||||
def export_csv_file(config: dict, results: list, csv_file: str) -> None:
|
||||
"""
|
||||
Log result to csv-file
|
||||
"""
|
||||
|
@@ -2,7 +2,7 @@
|
||||
This module contains the class to persist trades into SQLite
|
||||
"""
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from decimal import Decimal
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
@@ -1057,17 +1057,21 @@ class Trade(_DECL_BASE, LocalTrade):
|
||||
return total_open_stake_amount or 0
|
||||
|
||||
@staticmethod
|
||||
def get_overall_performance() -> List[Dict[str, Any]]:
|
||||
def get_overall_performance(minutes=None) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Returns List of dicts containing all Trades, including profit and trade count
|
||||
NOTE: Not supported in Backtesting.
|
||||
"""
|
||||
filters = [Trade.is_open.is_(False)]
|
||||
if minutes:
|
||||
start_date = datetime.now(timezone.utc) - timedelta(minutes=minutes)
|
||||
filters.append(Trade.close_date >= start_date)
|
||||
pair_rates = Trade.query.with_entities(
|
||||
Trade.pair,
|
||||
func.sum(Trade.close_profit).label('profit_sum'),
|
||||
func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
|
||||
func.count(Trade.pair).label('count')
|
||||
).filter(Trade.is_open.is_(False))\
|
||||
).filter(*filters)\
|
||||
.group_by(Trade.pair) \
|
||||
.order_by(desc('profit_sum_abs')) \
|
||||
.all()
|
||||
|
@@ -8,6 +8,7 @@ from typing import Any, Dict, List, Optional
|
||||
import arrow
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.configuration import PeriodicCache
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.misc import plural
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||
@@ -18,14 +19,15 @@ 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)
|
||||
|
||||
# Checked symbols cache (dictionary of ticker symbol => timestamp)
|
||||
self._symbolsChecked: Dict[str, int] = {}
|
||||
self._symbolsCheckFailed = PeriodicCache(maxsize=1000, ttl=86_400)
|
||||
|
||||
self._min_days_listed = pairlistconfig.get('min_days_listed', 10)
|
||||
self._max_days_listed = pairlistconfig.get('max_days_listed', None)
|
||||
|
||||
@@ -69,9 +71,12 @@ class AgeFilter(IPairList):
|
||||
:param tickers: Tickers (from exchange.get_tickers()). May be cached.
|
||||
:return: new allowlist
|
||||
"""
|
||||
needed_pairs = [(p, '1d') for p in pairlist if p not in self._symbolsChecked]
|
||||
needed_pairs = [
|
||||
(p, '1d') for p in pairlist
|
||||
if p not in self._symbolsChecked and p not in self._symbolsCheckFailed]
|
||||
if not needed_pairs:
|
||||
return pairlist
|
||||
# Remove pairs that have been removed before
|
||||
return [p for p in pairlist if p not in self._symbolsCheckFailed]
|
||||
|
||||
since_days = -(
|
||||
self._max_days_listed if self._max_days_listed else self._min_days_listed
|
||||
@@ -118,5 +123,6 @@ class AgeFilter(IPairList):
|
||||
" or more than "
|
||||
f"{self._max_days_listed} {plural(self._max_days_listed, 'day')}"
|
||||
) if self._max_days_listed else ''), logger.info)
|
||||
self._symbolsCheckFailed[pair] = arrow.utcnow().int_timestamp * 1000
|
||||
return False
|
||||
return False
|
||||
|
@@ -2,7 +2,7 @@
|
||||
Performance pair list filter
|
||||
"""
|
||||
import logging
|
||||
from typing import Dict, List
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import pandas as pd
|
||||
|
||||
@@ -15,6 +15,13 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class PerformanceFilter(IPairList):
|
||||
|
||||
def __init__(self, exchange, pairlistmanager,
|
||||
config: Dict[str, Any], pairlistconfig: Dict[str, Any],
|
||||
pairlist_pos: int) -> None:
|
||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||
|
||||
self._minutes = pairlistconfig.get('minutes', 0)
|
||||
|
||||
@property
|
||||
def needstickers(self) -> bool:
|
||||
"""
|
||||
@@ -40,7 +47,7 @@ class PerformanceFilter(IPairList):
|
||||
"""
|
||||
# Get the trading performance for pairs from database
|
||||
try:
|
||||
performance = pd.DataFrame(Trade.get_overall_performance())
|
||||
performance = pd.DataFrame(Trade.get_overall_performance(self._minutes))
|
||||
except AttributeError:
|
||||
# Performancefilter does not work in backtesting.
|
||||
self.log_once("PerformanceFilter is not available in this mode.", logger.warning)
|
||||
|
@@ -46,6 +46,12 @@ class Balances(BaseModel):
|
||||
value: float
|
||||
stake: str
|
||||
note: str
|
||||
starting_capital: float
|
||||
starting_capital_ratio: float
|
||||
starting_capital_pct: float
|
||||
starting_capital_fiat: float
|
||||
starting_capital_fiat_ratio: float
|
||||
starting_capital_fiat_pct: float
|
||||
|
||||
|
||||
class Count(BaseModel):
|
||||
|
@@ -459,6 +459,9 @@ class RPC:
|
||||
raise RPCException('Error getting current tickers.')
|
||||
|
||||
self._freqtrade.wallets.update(require_update=False)
|
||||
starting_capital = self._freqtrade.wallets.get_starting_balance()
|
||||
starting_cap_fiat = self._fiat_converter.convert_amount(
|
||||
starting_capital, stake_currency, fiat_display_currency) if self._fiat_converter else 0
|
||||
|
||||
for coin, balance in self._freqtrade.wallets.get_all_balances().items():
|
||||
if not balance.total:
|
||||
@@ -494,15 +497,25 @@ class RPC:
|
||||
else:
|
||||
raise RPCException('All balances are zero.')
|
||||
|
||||
symbol = fiat_display_currency
|
||||
value = self._fiat_converter.convert_amount(total, stake_currency,
|
||||
symbol) if self._fiat_converter else 0
|
||||
value = self._fiat_converter.convert_amount(
|
||||
total, stake_currency, fiat_display_currency) if self._fiat_converter else 0
|
||||
|
||||
starting_capital_ratio = 0.0
|
||||
starting_capital_ratio = (total / starting_capital) - 1 if starting_capital else 0.0
|
||||
starting_cap_fiat_ratio = (value / starting_cap_fiat) - 1 if starting_cap_fiat else 0.0
|
||||
|
||||
return {
|
||||
'currencies': output,
|
||||
'total': total,
|
||||
'symbol': symbol,
|
||||
'symbol': fiat_display_currency,
|
||||
'value': value,
|
||||
'stake': stake_currency,
|
||||
'starting_capital': starting_capital,
|
||||
'starting_capital_ratio': starting_capital_ratio,
|
||||
'starting_capital_pct': round(starting_capital_ratio * 100, 2),
|
||||
'starting_capital_fiat': starting_cap_fiat,
|
||||
'starting_capital_fiat_ratio': starting_cap_fiat_ratio,
|
||||
'starting_capital_fiat_pct': round(starting_cap_fiat_ratio * 100, 2),
|
||||
'note': 'Simulated balances' if self._freqtrade.config['dry_run'] else ''
|
||||
}
|
||||
|
||||
|
@@ -603,12 +603,15 @@ class Telegram(RPCHandler):
|
||||
|
||||
output = ''
|
||||
if self._config['dry_run']:
|
||||
output += (
|
||||
f"*Warning:* Simulated balances in Dry Mode.\n"
|
||||
"This mode is still experimental!\n"
|
||||
"Starting capital: "
|
||||
f"`{self._config['dry_run_wallet']}` {self._config['stake_currency']}.\n"
|
||||
)
|
||||
output += "*Warning:* Simulated balances in Dry Mode.\n"
|
||||
|
||||
output += ("Starting capital: "
|
||||
f"`{result['starting_capital']}` {self._config['stake_currency']}"
|
||||
)
|
||||
output += (f" `{result['starting_capital_fiat']}` "
|
||||
f"{self._config['fiat_display_currency']}.\n"
|
||||
) if result['starting_capital_fiat'] > 0 else '.\n'
|
||||
|
||||
total_dust_balance = 0
|
||||
total_dust_currencies = 0
|
||||
for curr in result['currencies']:
|
||||
@@ -641,9 +644,12 @@ class Telegram(RPCHandler):
|
||||
f"{round_coin_value(total_dust_balance, result['stake'], False)}`\n")
|
||||
|
||||
output += ("\n*Estimated Value*:\n"
|
||||
f"\t`{result['stake']}: {result['total']: .8f}`\n"
|
||||
f"\t`{result['stake']}: "
|
||||
f"{round_coin_value(result['total'], result['stake'], False)}`"
|
||||
f" `({result['starting_capital_pct']}%)`\n"
|
||||
f"\t`{result['symbol']}: "
|
||||
f"{round_coin_value(result['value'], result['symbol'], False)}`\n")
|
||||
f"{round_coin_value(result['value'], result['symbol'], False)}`"
|
||||
f" `({result['starting_capital_fiat_pct']}%)`\n")
|
||||
self._send_msg(output, reload_able=True, callback_path="update_balance",
|
||||
query=update.callback_query)
|
||||
except RPCException as e:
|
||||
|
@@ -3,5 +3,7 @@ from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timefr
|
||||
timeframe_to_prev_date, timeframe_to_seconds)
|
||||
from freqtrade.strategy.hyper import (BooleanParameter, CategoricalParameter, DecimalParameter,
|
||||
IntParameter, RealParameter)
|
||||
from freqtrade.strategy.informative_decorator import informative
|
||||
from freqtrade.strategy.interface import IStrategy
|
||||
from freqtrade.strategy.strategy_helper import merge_informative_pair, stoploss_from_open
|
||||
from freqtrade.strategy.strategy_helper import (merge_informative_pair, stoploss_from_absolute,
|
||||
stoploss_from_open)
|
||||
|
128
freqtrade/strategy/informative_decorator.py
Normal file
128
freqtrade/strategy/informative_decorator.py
Normal file
@@ -0,0 +1,128 @@
|
||||
from typing import Any, Callable, NamedTuple, Optional, Union
|
||||
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.strategy.strategy_helper import merge_informative_pair
|
||||
|
||||
|
||||
PopulateIndicators = Callable[[Any, DataFrame, dict], DataFrame]
|
||||
|
||||
|
||||
class InformativeData(NamedTuple):
|
||||
asset: Optional[str]
|
||||
timeframe: str
|
||||
fmt: Union[str, Callable[[Any], str], None]
|
||||
ffill: bool
|
||||
|
||||
|
||||
def informative(timeframe: str, asset: str = '',
|
||||
fmt: Optional[Union[str, Callable[[Any], str]]] = None,
|
||||
ffill: bool = True) -> Callable[[PopulateIndicators], PopulateIndicators]:
|
||||
"""
|
||||
A decorator for populate_indicators_Nn(self, dataframe, metadata), allowing these functions to
|
||||
define informative indicators.
|
||||
|
||||
Example usage:
|
||||
|
||||
@informative('1h')
|
||||
def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
||||
return dataframe
|
||||
|
||||
:param timeframe: Informative timeframe. Must always be equal or higher than strategy timeframe.
|
||||
:param asset: Informative asset, for example BTC, BTC/USDT, ETH/BTC. Do not specify to use
|
||||
current pair.
|
||||
:param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not
|
||||
specified, defaults to:
|
||||
* {base}_{quote}_{column}_{timeframe} if asset is specified.
|
||||
* {column}_{timeframe} if asset is not specified.
|
||||
Format string supports these format variables:
|
||||
* {asset} - full name of the asset, for example 'BTC/USDT'.
|
||||
* {base} - base currency in lower case, for example 'eth'.
|
||||
* {BASE} - same as {base}, except in upper case.
|
||||
* {quote} - quote currency in lower case, for example 'usdt'.
|
||||
* {QUOTE} - same as {quote}, except in upper case.
|
||||
* {column} - name of dataframe column.
|
||||
* {timeframe} - timeframe of informative dataframe.
|
||||
:param ffill: ffill dataframe after merging informative pair.
|
||||
"""
|
||||
_asset = asset
|
||||
_timeframe = timeframe
|
||||
_fmt = fmt
|
||||
_ffill = ffill
|
||||
|
||||
def decorator(fn: PopulateIndicators):
|
||||
informative_pairs = getattr(fn, '_ft_informative', [])
|
||||
informative_pairs.append(InformativeData(_asset, _timeframe, _fmt, _ffill))
|
||||
setattr(fn, '_ft_informative', informative_pairs)
|
||||
return fn
|
||||
return decorator
|
||||
|
||||
|
||||
def _format_pair_name(config, pair: str) -> str:
|
||||
return pair.format(stake_currency=config['stake_currency'],
|
||||
stake=config['stake_currency']).upper()
|
||||
|
||||
|
||||
def _create_and_merge_informative_pair(strategy, dataframe: DataFrame, metadata: dict,
|
||||
inf_data: InformativeData,
|
||||
populate_indicators: PopulateIndicators):
|
||||
asset = inf_data.asset or ''
|
||||
timeframe = inf_data.timeframe
|
||||
fmt = inf_data.fmt
|
||||
config = strategy.config
|
||||
|
||||
if asset:
|
||||
# Insert stake currency if needed.
|
||||
asset = _format_pair_name(config, asset)
|
||||
else:
|
||||
# Not specifying an asset will define informative dataframe for current pair.
|
||||
asset = metadata['pair']
|
||||
|
||||
if '/' in asset:
|
||||
base, quote = asset.split('/')
|
||||
else:
|
||||
# When futures are supported this may need reevaluation.
|
||||
# base, quote = asset, ''
|
||||
raise OperationalException('Not implemented.')
|
||||
|
||||
# Default format. This optimizes for the common case: informative pairs using same stake
|
||||
# currency. When quote currency matches stake currency, column name will omit base currency.
|
||||
# This allows easily reconfiguring strategy to use different base currency. In a rare case
|
||||
# where it is desired to keep quote currency in column name at all times user should specify
|
||||
# fmt='{base}_{quote}_{column}_{timeframe}' format or similar.
|
||||
if not fmt:
|
||||
fmt = '{column}_{timeframe}' # Informatives of current pair
|
||||
if inf_data.asset:
|
||||
fmt = '{base}_{quote}_' + fmt # Informatives of other pairs
|
||||
|
||||
inf_metadata = {'pair': asset, 'timeframe': timeframe}
|
||||
inf_dataframe = strategy.dp.get_pair_dataframe(asset, timeframe)
|
||||
inf_dataframe = populate_indicators(strategy, inf_dataframe, inf_metadata)
|
||||
|
||||
formatter: Any = None
|
||||
if callable(fmt):
|
||||
formatter = fmt # A custom user-specified formatter function.
|
||||
else:
|
||||
formatter = fmt.format # A default string formatter.
|
||||
|
||||
fmt_args = {
|
||||
'BASE': base.upper(),
|
||||
'QUOTE': quote.upper(),
|
||||
'base': base.lower(),
|
||||
'quote': quote.lower(),
|
||||
'asset': asset,
|
||||
'timeframe': timeframe,
|
||||
}
|
||||
inf_dataframe.rename(columns=lambda column: formatter(column=column, **fmt_args),
|
||||
inplace=True)
|
||||
|
||||
date_column = formatter(column='date', **fmt_args)
|
||||
if date_column in dataframe.columns:
|
||||
raise OperationalException(f'Duplicate column name {date_column} exists in '
|
||||
f'dataframe! Ensure column names are unique!')
|
||||
dataframe = merge_informative_pair(dataframe, inf_dataframe, strategy.timeframe, timeframe,
|
||||
ffill=inf_data.ffill, append_timeframe=False,
|
||||
date_column=date_column)
|
||||
return dataframe
|
@@ -19,6 +19,9 @@ from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
|
||||
from freqtrade.exchange.exchange import timeframe_to_next_date
|
||||
from freqtrade.persistence import PairLocks, Trade
|
||||
from freqtrade.strategy.hyper import HyperStrategyMixin
|
||||
from freqtrade.strategy.informative_decorator import (InformativeData, PopulateIndicators,
|
||||
_create_and_merge_informative_pair,
|
||||
_format_pair_name)
|
||||
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
||||
from freqtrade.wallets import Wallets
|
||||
|
||||
@@ -118,7 +121,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
# Class level variables (intentional) containing
|
||||
# the dataprovider (dp) (access to other candles, historic data, ...)
|
||||
# and wallets - access to the current balance.
|
||||
dp: Optional[DataProvider] = None
|
||||
dp: Optional[DataProvider]
|
||||
wallets: Optional[Wallets] = None
|
||||
# Filled from configuration
|
||||
stake_currency: str
|
||||
@@ -134,6 +137,24 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
self._last_candle_seen_per_pair: Dict[str, datetime] = {}
|
||||
super().__init__(config)
|
||||
|
||||
# Gather informative pairs from @informative-decorated methods.
|
||||
self._ft_informative: List[Tuple[InformativeData, PopulateIndicators]] = []
|
||||
for attr_name in dir(self.__class__):
|
||||
cls_method = getattr(self.__class__, attr_name)
|
||||
if not callable(cls_method):
|
||||
continue
|
||||
informative_data_list = getattr(cls_method, '_ft_informative', None)
|
||||
if not isinstance(informative_data_list, list):
|
||||
# Type check is required because mocker would return a mock object that evaluates to
|
||||
# True, confusing this code.
|
||||
continue
|
||||
strategy_timeframe_minutes = timeframe_to_minutes(self.timeframe)
|
||||
for informative_data in informative_data_list:
|
||||
if timeframe_to_minutes(informative_data.timeframe) < strategy_timeframe_minutes:
|
||||
raise OperationalException('Informative timeframe must be equal or higher than '
|
||||
'strategy timeframe!')
|
||||
self._ft_informative.append((informative_data, cls_method))
|
||||
|
||||
@abstractmethod
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
"""
|
||||
@@ -377,6 +398,23 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
# END - Intended to be overridden by strategy
|
||||
###
|
||||
|
||||
def gather_informative_pairs(self) -> ListPairsWithTimeframes:
|
||||
"""
|
||||
Internal method which gathers all informative pairs (user or automatically defined).
|
||||
"""
|
||||
informative_pairs = self.informative_pairs()
|
||||
for inf_data, _ in self._ft_informative:
|
||||
if inf_data.asset:
|
||||
pair_tf = (_format_pair_name(self.config, inf_data.asset), inf_data.timeframe)
|
||||
informative_pairs.append(pair_tf)
|
||||
else:
|
||||
if not self.dp:
|
||||
raise OperationalException('@informative decorator with unspecified asset '
|
||||
'requires DataProvider instance.')
|
||||
for pair in self.dp.current_whitelist():
|
||||
informative_pairs.append((pair, inf_data.timeframe))
|
||||
return list(set(informative_pairs))
|
||||
|
||||
def get_strategy_name(self) -> str:
|
||||
"""
|
||||
Returns strategy class name
|
||||
@@ -786,10 +824,11 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
Does not run advise_buy or advise_sell!
|
||||
Used by optimize operations only, not during dry / live runs.
|
||||
Using .copy() to get a fresh copy of the dataframe for every strategy run.
|
||||
Also copy on output to avoid PerformanceWarnings pandas 1.3.0 started to show.
|
||||
Has positive effects on memory usage for whatever reason - also when
|
||||
using only one strategy.
|
||||
"""
|
||||
return {pair: self.advise_indicators(pair_data.copy(), {'pair': pair})
|
||||
return {pair: self.advise_indicators(pair_data.copy(), {'pair': pair}).copy()
|
||||
for pair, pair_data in data.items()}
|
||||
|
||||
def advise_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
@@ -801,6 +840,12 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
:return: a Dataframe with all mandatory indicators for the strategies
|
||||
"""
|
||||
logger.debug(f"Populating indicators for pair {metadata.get('pair')}.")
|
||||
|
||||
# call populate_indicators_Nm() which were tagged with @informative decorator.
|
||||
for inf_data, populate_fn in self._ft_informative:
|
||||
dataframe = _create_and_merge_informative_pair(
|
||||
self, dataframe, metadata, inf_data, populate_fn)
|
||||
|
||||
if self._populate_fun_len == 2:
|
||||
warnings.warn("deprecated - check out the Sample strategy to see "
|
||||
"the current function headers!", DeprecationWarning)
|
||||
|
@@ -4,7 +4,9 @@ from freqtrade.exchange import timeframe_to_minutes
|
||||
|
||||
|
||||
def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame,
|
||||
timeframe: str, timeframe_inf: str, ffill: bool = True) -> pd.DataFrame:
|
||||
timeframe: str, timeframe_inf: str, ffill: bool = True,
|
||||
append_timeframe: bool = True,
|
||||
date_column: str = 'date') -> pd.DataFrame:
|
||||
"""
|
||||
Correctly merge informative samples to the original dataframe, avoiding lookahead bias.
|
||||
|
||||
@@ -24,6 +26,8 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame,
|
||||
:param timeframe: Timeframe of the original pair sample.
|
||||
:param timeframe_inf: Timeframe of the informative pair sample.
|
||||
:param ffill: Forwardfill missing values - optional but usually required
|
||||
:param append_timeframe: Rename columns by appending timeframe.
|
||||
:param date_column: A custom date column name.
|
||||
:return: Merged dataframe
|
||||
:raise: ValueError if the secondary timeframe is shorter than the dataframe timeframe
|
||||
"""
|
||||
@@ -32,25 +36,29 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame,
|
||||
minutes = timeframe_to_minutes(timeframe)
|
||||
if minutes == minutes_inf:
|
||||
# No need to forwardshift if the timeframes are identical
|
||||
informative['date_merge'] = informative["date"]
|
||||
informative['date_merge'] = informative[date_column]
|
||||
elif minutes < minutes_inf:
|
||||
# Subtract "small" timeframe so merging is not delayed by 1 small candle
|
||||
# Detailed explanation in https://github.com/freqtrade/freqtrade/issues/4073
|
||||
informative['date_merge'] = (
|
||||
informative["date"] + pd.to_timedelta(minutes_inf, 'm') - pd.to_timedelta(minutes, 'm')
|
||||
informative[date_column] + pd.to_timedelta(minutes_inf, 'm') -
|
||||
pd.to_timedelta(minutes, 'm')
|
||||
)
|
||||
else:
|
||||
raise ValueError("Tried to merge a faster timeframe to a slower timeframe."
|
||||
"This would create new rows, and can throw off your regular indicators.")
|
||||
|
||||
# Rename columns to be unique
|
||||
informative.columns = [f"{col}_{timeframe_inf}" for col in informative.columns]
|
||||
date_merge = 'date_merge'
|
||||
if append_timeframe:
|
||||
date_merge = f'date_merge_{timeframe_inf}'
|
||||
informative.columns = [f"{col}_{timeframe_inf}" for col in informative.columns]
|
||||
|
||||
# Combine the 2 dataframes
|
||||
# all indicators on the informative sample MUST be calculated before this point
|
||||
dataframe = pd.merge(dataframe, informative, left_on='date',
|
||||
right_on=f'date_merge_{timeframe_inf}', how='left')
|
||||
dataframe = dataframe.drop(f'date_merge_{timeframe_inf}', axis=1)
|
||||
right_on=date_merge, how='left')
|
||||
dataframe = dataframe.drop(date_merge, axis=1)
|
||||
|
||||
if ffill:
|
||||
dataframe = dataframe.ffill()
|
||||
@@ -83,3 +91,28 @@ def stoploss_from_open(open_relative_stop: float, current_profit: float) -> floa
|
||||
|
||||
# negative stoploss values indicate the requested stop price is higher than the current price
|
||||
return max(stoploss, 0.0)
|
||||
|
||||
|
||||
def stoploss_from_absolute(stop_rate: float, current_rate: float) -> float:
|
||||
"""
|
||||
Given current price and desired stop price, return a stop loss value that is relative to current
|
||||
price.
|
||||
|
||||
The requested stop can be positive for a stop above the open price, or negative for
|
||||
a stop below the open price. The return value is always >= 0.
|
||||
|
||||
Returns 0 if the resulting stop price would be above the current price.
|
||||
|
||||
:param stop_rate: Stop loss price.
|
||||
:param current_rate: Current asset price.
|
||||
:return: Positive stop loss value relative to current price
|
||||
"""
|
||||
|
||||
# formula is undefined for current_rate 0, return maximum value
|
||||
if current_rate == 0:
|
||||
return 1
|
||||
|
||||
stoploss = 1 - (stop_rate / current_rate)
|
||||
|
||||
# negative stoploss values indicate the requested stop price is higher than the current price
|
||||
return max(stoploss, 0.0)
|
||||
|
Reference in New Issue
Block a user