Merge branch 'freqtrade:develop' into plot_hyperopt_stats

This commit is contained in:
Italo
2022-03-20 15:42:53 +00:00
committed by GitHub
48 changed files with 203 additions and 136 deletions

View File

@@ -51,7 +51,7 @@ ARGS_LIST_PAIRS = ["exchange", "print_list", "list_pairs_print_json", "print_one
"print_csv", "base_currencies", "quote_currencies", "list_pairs_all"]
ARGS_TEST_PAIRLIST = ["verbosity", "config", "quote_currencies", "print_one_column",
"list_pairs_print_json"]
"list_pairs_print_json", "exchange"]
ARGS_CREATE_USERDIR = ["user_data_dir", "reset"]

View File

@@ -117,7 +117,7 @@ AVAILABLE_CLI_OPTIONS = {
),
# Optimize common
"timeframe": Arg(
'-i', '--timeframe', '--ticker-interval',
'-i', '--timeframe',
help='Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).',
),
"timerange": Arg(
@@ -169,7 +169,7 @@ AVAILABLE_CLI_OPTIONS = {
"strategy_list": Arg(
'--strategy-list',
help='Provide a space-separated list of strategies to backtest. '
'Please note that ticker-interval needs to be set either in config '
'Please note that timeframe needs to be set either in config '
'or via command line. When using this together with `--export trades`, '
'the strategy-name is injected into the filename '
'(so `backtest-data.json` becomes `backtest-data-SampleStrategy.json`',

View File

@@ -100,16 +100,11 @@ def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None:
"from the edge configuration."
)
if 'ticker_interval' in config:
logger.warning(
"DEPRECATED: "
raise OperationalException(
"DEPRECATED: 'ticker_interval' detected. "
"Please use 'timeframe' instead of 'ticker_interval."
)
if 'timeframe' in config:
raise OperationalException(
"Both 'timeframe' and 'ticker_interval' detected."
"Please remove 'ticker_interval' from your configuration to continue operating."
)
config['timeframe'] = config['ticker_interval']
if 'protections' in config:
logger.warning("DEPRECATED: Setting 'protections' in the configuration is deprecated.")

View File

@@ -219,9 +219,11 @@ class Edge:
"""
final = []
for pair, info in self._cached_pairs.items():
if info.expectancy > float(self.edge_config.get('minimum_expectancy', 0.2)) and \
info.winrate > float(self.edge_config.get('minimum_winrate', 0.60)) and \
pair in pairs:
if (
info.expectancy > float(self.edge_config.get('minimum_expectancy', 0.2))
and info.winrate > float(self.edge_config.get('minimum_winrate', 0.60))
and pair in pairs
):
final.append(pair)
if self._final_pairs != final:
@@ -246,8 +248,8 @@ class Edge:
"""
final = []
for pair, info in self._cached_pairs.items():
if info.expectancy > float(self.edge_config.get('minimum_expectancy', 0.2)) and \
info.winrate > float(self.edge_config.get('minimum_winrate', 0.60)):
if (info.expectancy > float(self.edge_config.get('minimum_expectancy', 0.2)) and
info.winrate > float(self.edge_config.get('minimum_winrate', 0.60))):
final.append({
'Pair': pair,
'Winrate': info.winrate,

View File

@@ -376,7 +376,7 @@ class Exchange:
raise OperationalException(
'Could not load markets, therefore cannot start. '
'Please investigate the above error for more details.'
)
)
quote_currencies = self.get_quote_currencies()
if stake_currency not in quote_currencies:
raise OperationalException(
@@ -882,11 +882,11 @@ class Exchange:
raise OperationalException(e) from e
@retrier(retries=API_FETCH_ORDER_RETRY_COUNT)
def fetch_order(self, order_id: str, pair: str) -> Dict:
def fetch_order(self, order_id: str, pair: str, params={}) -> Dict:
if self._config['dry_run']:
return self.fetch_dry_run_order(order_id)
try:
order = self._api.fetch_order(order_id, pair)
order = self._api.fetch_order(order_id, pair, params=params)
self._log_exchange_response('fetch_order', order)
return order
except ccxt.OrderNotFound as e:
@@ -929,7 +929,7 @@ class Exchange:
and order.get('filled') == 0.0)
@retrier
def cancel_order(self, order_id: str, pair: str) -> Dict:
def cancel_order(self, order_id: str, pair: str, params={}) -> Dict:
if self._config['dry_run']:
try:
order = self.fetch_dry_run_order(order_id)
@@ -940,7 +940,7 @@ class Exchange:
return {}
try:
order = self._api.cancel_order(order_id, pair)
order = self._api.cancel_order(order_id, pair, params=params)
self._log_exchange_response('cancel_order', order)
return order
except ccxt.InvalidOrder as e:

View File

@@ -22,13 +22,34 @@ class Gateio(Exchange):
_ft_has: Dict = {
"ohlcv_candle_limit": 1000,
"ohlcv_volume_currency": "quote",
"stoploss_order_types": {"limit": "limit"},
"stoploss_on_exchange": True,
}
_headers = {'X-Gate-Channel-Id': 'freqtrade'}
def validate_ordertypes(self, order_types: Dict) -> None:
super().validate_ordertypes(order_types)
if any(v == 'market' for k, v in order_types.items()):
raise OperationalException(
f'Exchange {self.name} does not support market orders.')
f'Exchange {self.name} does not support market orders.')
def fetch_stoploss_order(self, order_id: str, pair: str, params={}) -> Dict:
return self.fetch_order(
order_id=order_id,
pair=pair,
params={'stop': True}
)
def cancel_stoploss_order(self, order_id: str, pair: str, params={}) -> Dict:
return self.cancel_order(
order_id=order_id,
pair=pair,
params={'stop': True}
)
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
"""
Verify stop_loss against stoploss-order value (limit or price)
Returns True if adjustment is necessary.
"""
return stop_loss > float(order['stopPrice'])

View File

@@ -1428,14 +1428,14 @@ class FreqtradeBot(LoggingMixin):
def handle_order_fee(self, trade: Trade, order_obj: Order, order: Dict[str, Any]) -> None:
# Try update amount (binance-fix)
try:
new_amount = self.get_real_amount(trade, order)
new_amount = self.get_real_amount(trade, order, order_obj)
if not isclose(safe_value_fallback(order, 'filled', 'amount'), new_amount,
abs_tol=constants.MATH_CLOSE_PREC):
order_obj.ft_fee_base = trade.amount - new_amount
except DependencyException as exception:
logger.warning("Could not update trade amount: %s", exception)
def get_real_amount(self, trade: Trade, order: Dict) -> float:
def get_real_amount(self, trade: Trade, order: Dict, order_obj: Order) -> float:
"""
Detect and update trade fee.
Calls trade.update_fee() upon correct detection.
@@ -1453,7 +1453,7 @@ class FreqtradeBot(LoggingMixin):
# use fee from order-dict if possible
if self.exchange.order_has_fee(order):
fee_cost, fee_currency, fee_rate = self.exchange.extract_cost_curr_rate(order)
logger.info(f"Fee for Trade {trade} [{order.get('side')}]: "
logger.info(f"Fee for Trade {trade} [{order_obj.ft_order_side}]: "
f"{fee_cost:.8g} {fee_currency} - rate: {fee_rate}")
if fee_rate is None or fee_rate < 0.02:
# Reject all fees that report as > 2%.
@@ -1465,17 +1465,18 @@ class FreqtradeBot(LoggingMixin):
return self.apply_fee_conditional(trade, trade_base_currency,
amount=order_amount, fee_abs=fee_cost)
return order_amount
return self.fee_detection_from_trades(trade, order, order_amount, order.get('trades', []))
return self.fee_detection_from_trades(
trade, order, order_obj, order_amount, order.get('trades', []))
def fee_detection_from_trades(self, trade: Trade, order: Dict, order_amount: float,
trades: List) -> float:
def fee_detection_from_trades(self, trade: Trade, order: Dict, order_obj: Order,
order_amount: float, trades: List) -> float:
"""
fee-detection fallback to Trades.
Either uses provided trades list or the result of fetch_my_trades to get correct fee.
"""
if not trades:
trades = self.exchange.get_trades_for_order(
self.exchange.get_order_id_conditional(order), trade.pair, trade.open_date)
self.exchange.get_order_id_conditional(order), trade.pair, order_obj.order_date)
if len(trades) == 0:
logger.info("Applying fee on amount for %s failed: myTrade-Dict empty found", trade)

View File

@@ -87,7 +87,7 @@ class Backtesting:
validate_config_consistency(self.config)
if "timeframe" not in self.config:
raise OperationalException("Timeframe (ticker interval) needs to be set in either "
raise OperationalException("Timeframe needs to be set in either "
"configuration or as cli argument `--timeframe 5m`")
self.timeframe = str(self.config.get('timeframe'))
self.timeframe_min = timeframe_to_minutes(self.timeframe)

View File

@@ -29,15 +29,13 @@ class IHyperOpt(ABC):
Class attributes you can use:
timeframe -> int: value of the timeframe to use for the strategy
"""
ticker_interval: str # DEPRECATED
timeframe: str
strategy: IStrategy
def __init__(self, config: dict) -> None:
self.config = config
# Assign ticker_interval to be used in hyperopt
IHyperOpt.ticker_interval = str(config['timeframe']) # DEPRECATED
# Assign timeframe to be used in hyperopt
IHyperOpt.timeframe = str(config['timeframe'])
def generate_estimator(self, dimensions: List[Dimension], **kwargs) -> EstimatorType:
@@ -192,7 +190,7 @@ class IHyperOpt(ABC):
Categorical([True, False], name='trailing_only_offset_is_reached'),
]
# This is needed for proper unpickling the class attribute ticker_interval
# This is needed for proper unpickling the class attribute timeframe
# which is set to the actual value by the resolver.
# Why do I still need such shamanic mantras in modern python?
def __getstate__(self):
@@ -202,5 +200,4 @@ class IHyperOpt(ABC):
def __setstate__(self, state):
self.__dict__.update(state)
IHyperOpt.ticker_interval = state['timeframe']
IHyperOpt.timeframe = state['timeframe']

View File

@@ -174,16 +174,17 @@ def drop_orders_table(engine, table_back_name: str):
def migrate_orders_table(engine, table_back_name: str, cols_order: List):
ft_fee_base = get_column_def(cols_order, 'ft_fee_base', 'null')
average = get_column_def(cols_order, 'average', 'null')
# let SQLAlchemy create the schema as required
with engine.begin() as connection:
connection.execute(text(f"""
insert into orders ( id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id,
status, symbol, order_type, side, price, amount, filled, average, remaining, cost,
order_date, order_filled_date, order_update_date, ft_fee_base)
status, symbol, order_type, side, price, amount, filled, average, remaining,
cost, order_date, order_filled_date, order_update_date, ft_fee_base)
select id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id,
status, symbol, order_type, side, price, amount, filled, null average, remaining, cost,
order_date, order_filled_date, order_update_date, {ft_fee_base}
status, symbol, order_type, side, price, amount, filled, {average} average, remaining,
cost, order_date, order_filled_date, order_update_date, {ft_fee_base} ft_fee_base
from {table_back_name}
"""))

View File

@@ -98,7 +98,7 @@ class AgeFilter(IPairList):
"""
Validate age for the ticker
:param pair: Pair that's currently validated
:param ticker: ticker dict as returned from ccxt.fetch_tickers()
:param daily_candles: Downloaded daily candles
:return: True if the pair can stay, false if it should be removed
"""
# Check symbol in cache

View File

@@ -51,7 +51,7 @@ class PrecisionFilter(IPairList):
:param ticker: ticker dict as returned from ccxt.fetch_tickers()
:return: True if the pair can stay, false if it should be removed
"""
stop_price = ticker['ask'] * self._stoploss
stop_price = ticker['last'] * self._stoploss
# Adjust stop-prices to precision
sp = self._exchange.price_to_precision(pair, stop_price)

View File

@@ -4,6 +4,7 @@ Spread pair list filter
import logging
from typing import Any, Dict
from freqtrade.exceptions import OperationalException
from freqtrade.plugins.pairlist.IPairList import IPairList
@@ -20,6 +21,12 @@ class SpreadFilter(IPairList):
self._max_spread_ratio = pairlistconfig.get('max_spread_ratio', 0.005)
self._enabled = self._max_spread_ratio != 0
if not self._exchange.exchange_has('fetchTickers'):
raise OperationalException(
'Exchange does not support fetchTickers, therefore SpreadFilter cannot be used.'
'Please edit your config and restart the bot.'
)
@property
def needstickers(self) -> bool:
"""

View File

@@ -90,7 +90,7 @@ class VolatilityFilter(IPairList):
"""
Validate trading range
:param pair: Pair that's currently validated
:param ticker: ticker dict as returned from ccxt.fetch_tickers()
:param daily_candles: Downloaded daily candles
:return: True if the pair can stay, false if it should be removed
"""
# Check symbol in cache

View File

@@ -88,7 +88,7 @@ class RangeStabilityFilter(IPairList):
"""
Validate trading range
:param pair: Pair that's currently validated
:param ticker: ticker dict as returned from ccxt.fetch_tickers()
:param daily_candles: Downloaded daily candles
:return: True if the pair can stay, false if it should be removed
"""
# Check symbol in cache

View File

@@ -44,7 +44,6 @@ class HyperOptLossResolver(IResolver):
extra_dir=config.get('hyperopt_path'))
# Assign timeframe to be used in hyperopt
hyperoptloss.__class__.ticker_interval = str(config['timeframe'])
hyperoptloss.__class__.timeframe = str(config['timeframe'])
return hyperoptloss

View File

@@ -45,14 +45,6 @@ class StrategyResolver(IResolver):
strategy_name, config=config,
extra_dir=config.get('strategy_path'))
if hasattr(strategy, 'ticker_interval') and not hasattr(strategy, 'timeframe'):
# Assign ticker_interval to timeframe to keep compatibility
if 'timeframe' not in config:
logger.warning(
"DEPRECATED: Please migrate to using 'timeframe' instead of 'ticker_interval'."
)
strategy.timeframe = strategy.ticker_interval
if strategy._ft_params_from_file:
# Set parameters from Hyperopt results file
params = strategy._ft_params_from_file
@@ -145,10 +137,6 @@ class StrategyResolver(IResolver):
"""
Normalize attributes to have the correct type.
"""
# Assign deprecated variable - to not break users code relying on this.
if hasattr(strategy, 'timeframe'):
strategy.ticker_interval = strategy.timeframe
# Sort and apply type conversions
if hasattr(strategy, 'minimal_roi'):
strategy.minimal_roi = dict(sorted(

View File

@@ -137,7 +137,7 @@ def show_config(rpc: Optional[RPC] = Depends(get_rpc_optional), config=Depends(g
def forcebuy(payload: ForceBuyPayload, rpc: RPC = Depends(get_rpc)):
ordertype = payload.ordertype.value if payload.ordertype else None
stake_amount = payload.stakeamount if payload.stakeamount else None
entry_tag = payload.entry_tag if payload.entry_tag else None
entry_tag = payload.entry_tag if payload.entry_tag else 'forceentry'
trade = rpc._rpc_forcebuy(payload.pair, payload.price, ordertype, stake_amount, entry_tag)

View File

@@ -582,7 +582,7 @@ class RPC:
else:
try:
pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency)
rate = tickers.get(pair, {}).get('bid', None)
rate = tickers.get(pair, {}).get('last', None)
if rate:
if pair.startswith(stake_currency) and not pair.endswith(stake_currency):
rate = 1.0 / rate
@@ -713,7 +713,7 @@ class RPC:
def _rpc_forcebuy(self, pair: str, price: Optional[float], order_type: Optional[str] = None,
stake_amount: Optional[float] = None,
buy_tag: Optional[str] = None) -> Optional[Trade]:
buy_tag: Optional[str] = 'forceentry') -> Optional[Trade]:
"""
Handler for forcebuy <asset> <price>
Buys a pair trade at the given or current price

View File

@@ -55,7 +55,7 @@ class IStrategy(ABC, HyperStrategyMixin):
Attributes you can use:
minimal_roi -> Dict: Minimal ROI designed for the strategy
stoploss -> float: optimal stoploss designed for the strategy
timeframe -> str: value of the timeframe (ticker interval) to use with the strategy
timeframe -> str: value of the timeframe to use with the strategy
"""
# Strategy interface version
# Default to version 2
@@ -81,7 +81,6 @@ class IStrategy(ABC, HyperStrategyMixin):
use_custom_stoploss: bool = False
# associated timeframe
ticker_interval: str # DEPRECATED
timeframe: str
# Optional order types