Merge branch 'develop' into pr/mkavinkumar1/6545

This commit is contained in:
Matthias
2022-07-11 11:20:31 +02:00
28 changed files with 13127 additions and 12211 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -46,6 +46,7 @@ MAP_EXCHANGE_CHILDCLASS = {
'binanceje': 'binance',
'binanceusdm': 'binance',
'okex': 'okx',
'gate': 'gateio',
}
SUPPORTED_EXCHANGES = [
@@ -63,17 +64,16 @@ EXCHANGE_HAS_REQUIRED = [
'fetchOrder',
'cancelOrder',
'createOrder',
# 'createLimitOrder', 'createMarketOrder',
'fetchBalance',
# Public endpoints
'loadMarkets',
'fetchOHLCV',
]
EXCHANGE_HAS_OPTIONAL = [
# Private
'fetchMyTrades', # Trades for order - fee detection
# 'createLimitOrder', 'createMarketOrder', # Either OR for orders
# 'setLeverage', # Margin/Futures trading
# 'setMarginMode', # Margin/Futures trading
# 'fetchFundingHistory', # Futures trading

View File

@@ -77,6 +77,7 @@ class Exchange:
"mark_ohlcv_price": "mark",
"mark_ohlcv_timeframe": "8h",
"ccxt_futures_name": "swap",
"fee_cost_in_contracts": False, # Fee cost needs contract conversion
"needs_trading_fees": False, # use fetch_trading_fees to cache fees
}
_ft_has: Dict = {}
@@ -585,10 +586,13 @@ class Exchange:
"""
Checks if order-types configured in strategy/config are supported
"""
if any(v == 'market' for k, v in order_types.items()):
if not self.exchange_has('createMarketOrder'):
raise OperationalException(
f'Exchange {self.name} does not support market orders.')
# TODO: Reenable once ccxt fixes createMarketOrder assignment - as well as
# Revert the change in test_validate_ordertypes.
# if any(v == 'market' for k, v in order_types.items()):
# if not self.exchange_has('createMarketOrder'):
# raise OperationalException(
# f'Exchange {self.name} does not support market orders.')
if (order_types.get("stoploss_on_exchange")
and not self._ft_has.get("stoploss_on_exchange", False)):
@@ -1661,27 +1665,35 @@ class Exchange:
and order['fee']['cost'] is not None
)
def calculate_fee_rate(self, order: Dict) -> Optional[float]:
def calculate_fee_rate(
self, fee: Dict, symbol: str, cost: float, amount: float) -> Optional[float]:
"""
Calculate fee rate if it's not given by the exchange.
:param order: Order or trade (one trade) dict
:param fee: ccxt Fee dict - must contain cost / currency / rate
:param symbol: Symbol of the order
:param cost: Total cost of the order
:param amount: Amount of the order
"""
if order['fee'].get('rate') is not None:
return order['fee'].get('rate')
fee_curr = order['fee']['currency']
if fee.get('rate') is not None:
return fee.get('rate')
fee_curr = fee.get('currency')
if fee_curr is None:
return None
fee_cost = fee['cost']
if self._ft_has['fee_cost_in_contracts']:
# Convert cost via "contracts" conversion
fee_cost = self._contracts_to_amount(symbol, fee['cost'])
# Calculate fee based on order details
if fee_curr in self.get_pair_base_currency(order['symbol']):
if fee_curr == self.get_pair_base_currency(symbol):
# Base currency - divide by amount
return round(
order['fee']['cost'] / safe_value_fallback2(order, order, 'filled', 'amount'), 8)
elif fee_curr in self.get_pair_quote_currency(order['symbol']):
return round(fee['cost'] / amount, 8)
elif fee_curr == self.get_pair_quote_currency(symbol):
# Quote currency - divide by cost
return round(self._contracts_to_amount(
order['symbol'], order['fee']['cost']) / order['cost'],
8) if order['cost'] else None
return round(fee_cost / cost, 8) if cost else None
else:
# If Fee currency is a different currency
if not order['cost']:
if not cost:
# If cost is None or 0.0 -> falsy, return None
return None
try:
@@ -1693,19 +1705,28 @@ class Exchange:
fee_to_quote_rate = self._config['exchange'].get('unknown_fee_rate', None)
if not fee_to_quote_rate:
return None
return round((self._contracts_to_amount(
order['symbol'], order['fee']['cost']) * fee_to_quote_rate) / order['cost'], 8)
return round((fee_cost * fee_to_quote_rate) / cost, 8)
def extract_cost_curr_rate(self, order: Dict) -> Tuple[float, str, Optional[float]]:
def extract_cost_curr_rate(self, fee: Dict, symbol: str, cost: float,
amount: float) -> Tuple[float, str, Optional[float]]:
"""
Extract tuple of cost, currency, rate.
Requires order_has_fee to run first!
:param order: Order or trade (one trade) dict
:param fee: ccxt Fee dict - must contain cost / currency / rate
:param symbol: Symbol of the order
:param cost: Total cost of the order
:param amount: Amount of the order
:return: Tuple with cost, currency, rate of the given fee dict
"""
return (order['fee']['cost'],
order['fee']['currency'],
self.calculate_fee_rate(order))
return (fee['cost'],
fee['currency'],
self.calculate_fee_rate(
fee,
symbol,
cost,
amount
)
)
# Historic data

View File

@@ -32,7 +32,8 @@ class Gateio(Exchange):
}
_ft_has_futures: Dict = {
"needs_trading_fees": True
"needs_trading_fees": True,
"fee_cost_in_contracts": False, # Set explicitly to false for clarity
}
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [

View File

@@ -28,6 +28,7 @@ class Okx(Exchange):
}
_ft_has_futures: Dict = {
"tickers_have_quoteVolume": False,
"fee_cost_in_contracts": True,
}
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [

View File

@@ -657,7 +657,7 @@ class FreqtradeBot(LoggingMixin):
pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested,
time_in_force=time_in_force, current_time=datetime.now(timezone.utc),
entry_tag=enter_tag, side=trade_side):
logger.info(f"User requested abortion of buying {pair}")
logger.info(f"User denied entry for {pair}.")
return False
order = self.exchange.create_order(
pair=pair,
@@ -837,7 +837,7 @@ class FreqtradeBot(LoggingMixin):
pair=pair, current_time=datetime.now(timezone.utc),
current_rate=enter_limit_requested, proposed_stake=stake_amount,
min_stake=min_stake_amount, max_stake=min(max_stake_amount, stake_available),
entry_tag=entry_tag, side=trade_side
leverage=leverage, entry_tag=entry_tag, side=trade_side
)
stake_amount = self.wallets.validate_stake_amount(
@@ -986,6 +986,29 @@ class FreqtradeBot(LoggingMixin):
logger.debug(f'Found no {exit_signal_type} signal for %s.', trade)
return False
def _check_and_execute_exit(self, trade: Trade, exit_rate: float,
enter: bool, exit_: bool, exit_tag: Optional[str]) -> bool:
"""
Check and execute trade exit
"""
exits: List[ExitCheckTuple] = self.strategy.should_exit(
trade,
exit_rate,
datetime.now(timezone.utc),
enter=enter,
exit_=exit_,
force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0
)
for should_exit in exits:
if should_exit.exit_flag:
exit_tag1 = exit_tag if should_exit.exit_type == ExitType.EXIT_SIGNAL else None
logger.info(f'Exit for {trade.pair} detected. Reason: {should_exit.exit_type}'
f'{f" Tag: {exit_tag1}" if exit_tag1 is not None else ""}')
exited = self.execute_trade_exit(trade, exit_rate, should_exit, exit_tag=exit_tag1)
if exited:
return True
return False
def create_stoploss_order(self, trade: Trade, stop_price: float) -> bool:
"""
Abstracts creating stoploss orders from the logic.
@@ -1137,28 +1160,6 @@ class FreqtradeBot(LoggingMixin):
logger.warning(f"Could not create trailing stoploss order "
f"for pair {trade.pair}.")
def _check_and_execute_exit(self, trade: Trade, exit_rate: float,
enter: bool, exit_: bool, exit_tag: Optional[str]) -> bool:
"""
Check and execute trade exit
"""
exits: List[ExitCheckTuple] = self.strategy.should_exit(
trade,
exit_rate,
datetime.now(timezone.utc),
enter=enter,
exit_=exit_,
force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0
)
for should_exit in exits:
if should_exit.exit_flag:
logger.info(f'Exit for {trade.pair} detected. Reason: {should_exit.exit_type}'
f'{f" Tag: {exit_tag}" if exit_tag is not None else ""}')
exited = self.execute_trade_exit(trade, exit_rate, should_exit, exit_tag=exit_tag)
if exited:
return True
return False
def manage_open_orders(self) -> None:
"""
Management of open orders on exchange. Unfilled orders might be cancelled if timeout
@@ -1496,7 +1497,7 @@ class FreqtradeBot(LoggingMixin):
time_in_force=time_in_force, exit_reason=exit_reason,
sell_reason=exit_reason, # sellreason -> compatibility
current_time=datetime.now(timezone.utc)):
logger.info(f"User requested abortion of {trade.pair} exit.")
logger.info(f"User denied exit for {trade.pair}.")
return False
try:
@@ -1795,7 +1796,8 @@ class FreqtradeBot(LoggingMixin):
trade_base_currency = self.exchange.get_pair_base_currency(trade.pair)
# 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)
fee_cost, fee_currency, fee_rate = self.exchange.extract_cost_curr_rate(
order['fee'], order['symbol'], order['cost'], order_obj.safe_filled)
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:
@@ -1833,7 +1835,15 @@ class FreqtradeBot(LoggingMixin):
for exectrade in trades:
amount += exectrade['amount']
if self.exchange.order_has_fee(exectrade):
fee_cost_, fee_currency, fee_rate_ = self.exchange.extract_cost_curr_rate(exectrade)
# Prefer singular fee
fees = [exectrade['fee']]
else:
fees = exectrade.get('fees', [])
for fee in fees:
fee_cost_, fee_currency, fee_rate_ = self.exchange.extract_cost_curr_rate(
fee, exectrade['symbol'], exectrade['cost'], exectrade['amount']
)
fee_cost += fee_cost_
if fee_rate_ is not None:
fee_rate_array.append(fee_rate_)

View File

@@ -189,6 +189,7 @@ class Backtesting:
self.strategy.order_types['stoploss_on_exchange'] = False
self.strategy.ft_bot_start()
strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)()
def _load_protections(self, strategy: IStrategy):
if self.config.get('enable_protections', False):
@@ -749,7 +750,7 @@ class Backtesting:
pair=pair, current_time=current_time, current_rate=propose_rate,
proposed_stake=stake_amount, min_stake=min_stake_amount,
max_stake=min(stake_available, max_stake_amount),
entry_tag=entry_tag, side=direction)
leverage=leverage, entry_tag=entry_tag, side=direction)
stake_amount_val = self.wallets.validate_stake_amount(
pair=pair,
@@ -1173,8 +1174,6 @@ class Backtesting:
backtest_start_time = datetime.now(timezone.utc)
self._set_strategy(strat)
strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)()
# Use max_open_trades in backtesting, except --disable-max-market-positions is set
if self.config.get('use_max_market_positions', True):
# Must come from strategy config, as the strategy may modify this setting.

View File

@@ -1,9 +1,10 @@
import logging
from typing import List
from sqlalchemy import inspect, text
from sqlalchemy import inspect, select, text, tuple_, update
from freqtrade.exceptions import OperationalException
from freqtrade.persistence.trade_model import Order, Trade
logger = logging.getLogger(__name__)
@@ -252,31 +253,31 @@ def set_sqlite_to_wal(engine):
def fix_old_dry_orders(engine):
with engine.begin() as connection:
connection.execute(
text(
"""
update orders
set ft_is_open = 0
where ft_is_open = 1 and (ft_trade_id, order_id) not in (
select id, stoploss_order_id from trades where stoploss_order_id is not null
) and ft_order_side = 'stoploss'
and order_id like 'dry_%'
"""
)
)
connection.execute(
text(
"""
update orders
set ft_is_open = 0
where ft_is_open = 1
and (ft_trade_id, order_id) not in (
select id, open_order_id from trades where open_order_id is not null
) and ft_order_side != 'stoploss'
and order_id like 'dry_%'
"""
)
)
stmt = update(Order).where(
Order.ft_is_open.is_(True),
tuple_(Order.ft_trade_id, Order.order_id).not_in(
select(
Trade.id, Trade.stoploss_order_id
).where(Trade.stoploss_order_id.is_not(None))
),
Order.ft_order_side == 'stoploss',
Order.order_id.like('dry%'),
).values(ft_is_open=False)
connection.execute(stmt)
stmt = update(Order).where(
Order.ft_is_open.is_(True),
tuple_(Order.ft_trade_id, Order.order_id).not_in(
select(
Trade.id, Trade.open_order_id
).where(Trade.open_order_id.is_not(None))
),
Order.ft_order_side != 'stoploss',
Order.order_id.like('dry%')
).values(ft_is_open=False)
connection.execute(stmt)
def check_migrate(engine, decl_base, previous_tables) -> None:

View File

@@ -877,7 +877,7 @@ class LocalTrade():
self.open_rate = total_stake / total_amount
self.stake_amount = total_stake / (self.leverage or 1.0)
self.amount = total_amount
self.fee_open_cost = self.fee_open * self.stake_amount
self.fee_open_cost = self.fee_open * total_stake
self.recalc_open_trade_value()
if self.stop_loss_pct is not None and self.open_rate is not None:
self.adjust_stop_loss(self.open_rate, self.stop_loss_pct)

View File

@@ -191,6 +191,7 @@ def detect_parameters(
and attr.category is not None and attr.category != category):
raise OperationalException(
f'Inconclusive parameter name {attr_name}, category: {attr.category}.')
if (category == attr.category or
(attr_name.startswith(category + '_') and attr.category is None)):
yield attr_name, attr

View File

@@ -442,7 +442,8 @@ class IStrategy(ABC, HyperStrategyMixin):
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
proposed_stake: float, min_stake: Optional[float], max_stake: float,
entry_tag: Optional[str], side: str, **kwargs) -> float:
leverage: float, entry_tag: Optional[str], side: str,
**kwargs) -> float:
"""
Customize stake size for each new trade.
@@ -452,6 +453,7 @@ class IStrategy(ABC, HyperStrategyMixin):
:param proposed_stake: A stake amount proposed by the bot.
:param min_stake: Minimal stake size allowed by exchange.
:param max_stake: Balance available for trading.
:param leverage: Leverage selected for this trade.
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
:param side: 'long' or 'short' - indicating the direction of the proposed trade
:return: A stake size, which is between min_stake and max_stake.

View File

@@ -79,9 +79,10 @@ def custom_exit_price(self, pair: str, trade: 'Trade',
"""
return proposed_rate
def custom_stake_amount(self, pair: str, current_time: 'datetime', current_rate: float,
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
proposed_stake: float, min_stake: Optional[float], max_stake: float,
entry_tag: 'Optional[str]', side: str, **kwargs) -> float:
leverage: float, entry_tag: Optional[str], side: str,
**kwargs) -> float:
"""
Customize stake size for each new trade.
@@ -91,6 +92,7 @@ def custom_stake_amount(self, pair: str, current_time: 'datetime', current_rate:
:param proposed_stake: A stake amount proposed by the bot.
:param min_stake: Minimal stake size allowed by exchange.
:param max_stake: Balance available for trading.
:param leverage: Leverage selected for this trade.
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
:param side: 'long' or 'short' - indicating the direction of the proposed trade
:return: A stake size, which is between min_stake and max_stake.