Merge branch 'develop' into pr/mkavinkumar1/6545

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

View File

@ -13,11 +13,11 @@ repos:
- id: mypy - id: mypy
exclude: build_helpers exclude: build_helpers
additional_dependencies: additional_dependencies:
- types-cachetools==5.0.2 - types-cachetools==5.2.1
- types-filelock==3.2.7 - types-filelock==3.2.7
- types-requests==2.27.30 - types-requests==2.28.0
- types-tabulate==0.8.9 - types-tabulate==0.8.11
- types-python-dateutil==2.8.17 - types-python-dateutil==2.8.18
# stages: [push] # stages: [push]
- repo: https://github.com/pycqa/isort - repo: https://github.com/pycqa/isort

View File

@ -20,7 +20,9 @@ All profit calculations of Freqtrade include fees. For Backtesting / Hyperopt /
## Bot execution logic ## Bot execution logic
Starting freqtrade in dry-run or live mode (using `freqtrade trade`) will start the bot and start the bot iteration loop. Starting freqtrade in dry-run or live mode (using `freqtrade trade`) will start the bot and start the bot iteration loop.
By default, loop runs every few seconds (`internals.process_throttle_secs`) and does roughly the following in the following sequence: This will also run the `bot_start()` callback.
By default, the bot loop runs every few seconds (`internals.process_throttle_secs`) and performs the following actions:
* Fetch open trades from persistence. * Fetch open trades from persistence.
* Calculate current list of tradable pairs. * Calculate current list of tradable pairs.
@ -54,6 +56,7 @@ This loop will be repeated again and again until the bot is stopped.
[backtesting](backtesting.md) or [hyperopt](hyperopt.md) do only part of the above logic, since most of the trading operations are fully simulated. [backtesting](backtesting.md) or [hyperopt](hyperopt.md) do only part of the above logic, since most of the trading operations are fully simulated.
* Load historic data for configured pairlist. * Load historic data for configured pairlist.
* Calls `bot_start()` once.
* Calls `bot_loop_start()` once. * Calls `bot_loop_start()` once.
* Calculate indicators (calls `populate_indicators()` once per pair). * Calculate indicators (calls `populate_indicators()` once per pair).
* Calculate entry / exit signals (calls `populate_entry_trend()` and `populate_exit_trend()` once per pair). * Calculate entry / exit signals (calls `populate_entry_trend()` and `populate_exit_trend()` once per pair).

View File

@ -334,7 +334,7 @@ lev_tiers = exchange.fetch_leverage_tiers()
# Assumes this is running in the root of the repository. # Assumes this is running in the root of the repository.
file = Path('freqtrade/exchange/binance_leverage_tiers.json') file = Path('freqtrade/exchange/binance_leverage_tiers.json')
json.dump(lev_tiers, file.open('w'), indent=2) json.dump(dict(sorted(lev_tiers.items())), file.open('w'), indent=2)
``` ```

View File

@ -272,6 +272,7 @@ The last one we call `trigger` and use it to decide which buy trigger we want to
!!! Note "Parameter space assignment" !!! Note "Parameter space assignment"
Parameters must either be assigned to a variable named `buy_*` or `sell_*` - or contain `space='buy'` | `space='sell'` to be assigned to a space correctly. Parameters must either be assigned to a variable named `buy_*` or `sell_*` - or contain `space='buy'` | `space='sell'` to be assigned to a space correctly.
If no parameter is available for a space, you'll receive the error that no space was found when running hyperopt. If no parameter is available for a space, you'll receive the error that no space was found when running hyperopt.
Parameters with unclear space (e.g. `adx_period = IntParameter(4, 24, default=14)` - no explicit nor implicit space) will not be detected and will therefore be ignored.
So let's write the buy strategy using these values: So let's write the buy strategy using these values:

View File

@ -1,5 +1,5 @@
mkdocs==1.3.0 mkdocs==1.3.0
mkdocs-material==8.3.6 mkdocs-material==8.3.9
mdx_truly_sane_lists==1.2 mdx_truly_sane_lists==1.2
pymdown-extensions==9.5 pymdown-extensions==9.5
jinja2==3.1.2 jinja2==3.1.2

View File

@ -130,7 +130,7 @@ In summary: The stoploss will be adjusted to be always be -10% of the highest ob
### Trailing stop loss, custom positive loss ### Trailing stop loss, custom positive loss
It is also possible to have a default stop loss, when you are in the red with your buy (buy - fee), but once you hit positive result the system will utilize a new stop loss, which can have a different value. You could also have a default stop loss when you are in the red with your buy (buy - fee), but once you hit a positive result (or an offset you define) the system will utilize a new stop loss, which can have a different value.
For example, your default stop loss is -10%, but once you have more than 0% profit (example 0.1%) a different trailing stoploss will be used. For example, your default stop loss is -10%, but once you have more than 0% profit (example 0.1%) a different trailing stoploss will be used.
!!! Note !!! Note
@ -142,6 +142,8 @@ Both values require `trailing_stop` to be set to true and `trailing_stop_positiv
stoploss = -0.10 stoploss = -0.10
trailing_stop = True trailing_stop = True
trailing_stop_positive = 0.02 trailing_stop_positive = 0.02
trailing_stop_positive_offset = 0.0
trailing_only_offset_is_reached = False # Default - not necessary for this example
``` ```
For example, simplified math: For example, simplified math:
@ -156,11 +158,31 @@ For example, simplified math:
The 0.02 would translate to a -2% stop loss. The 0.02 would translate to a -2% stop loss.
Before this, `stoploss` is used for the trailing stoploss. Before this, `stoploss` is used for the trailing stoploss.
!!! Tip "Use an offset to change your stoploss"
Use `trailing_stop_positive_offset` to ensure that your new trailing stoploss will be in profit by setting `trailing_stop_positive_offset` higher than `trailing_stop_positive`. Your first new stoploss value will then already have locked in profits.
Example with simplified math:
``` python
stoploss = -0.10
trailing_stop = True
trailing_stop_positive = 0.02
trailing_stop_positive_offset = 0.03
```
* the bot buys an asset at a price of 100$
* the stop loss is defined at -10%, so the stop loss would get triggered once the asset drops below 90$
* assuming the asset now increases to 102$
* the stoploss will now be at 91.8$ - 10% below the highest observed rate
* assuming the asset now increases to 103.5$ (above the offset configured)
* the stop loss will now be -2% of 103$ = 101.42$
* now the asset drops in value to 102\$, the stop loss will still be 101.42$ and would trigger once price breaks below 101.42$
### Trailing stop loss only once the trade has reached a certain offset ### Trailing stop loss only once the trade has reached a certain offset
It is also possible to use a static stoploss until the offset is reached, and then trail the trade to take profits once the market turns. You can also keep a static stoploss until the offset is reached, and then trail the trade to take profits once the market turns.
If `"trailing_only_offset_is_reached": true` then the trailing stoploss is only activated once the offset is reached. Until then, the stoploss remains at the configured `stoploss`. If `trailing_only_offset_is_reached = True` then the trailing stoploss is only activated once the offset is reached. Until then, the stoploss remains at the configured `stoploss`.
This option can be used with or without `trailing_stop_positive`, but uses `trailing_stop_positive_offset` as offset. This option can be used with or without `trailing_stop_positive`, but uses `trailing_stop_positive_offset` as offset.
``` python ``` python
@ -203,7 +225,6 @@ If price moves 1% - you've lost 10$ of your own capital - therfore stoploss will
Make sure to be aware of this, and avoid using too tight stoploss (at 10x leverage, 10% risk may be too little to allow the trade to "breath" a little). Make sure to be aware of this, and avoid using too tight stoploss (at 10x leverage, 10% risk may be too little to allow the trade to "breath" a little).
## Changing stoploss on open trades ## Changing stoploss on open trades
A stoploss on an open trade can be changed by changing the value in the configuration or strategy and use the `/reload_config` command (alternatively, completely stopping and restarting the bot also works). A stoploss on an open trade can be changed by changing the value in the configuration or strategy and use the `/reload_config` command (alternatively, completely stopping and restarting the bot also works).

View File

@ -82,8 +82,9 @@ Called before entering a trade, makes it possible to manage your position size w
```python ```python
class AwesomeStrategy(IStrategy): class AwesomeStrategy(IStrategy):
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: float, max_stake: 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:
dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe) dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe)
current_candle = dataframe.iloc[-1].squeeze() current_candle = dataframe.iloc[-1].squeeze()
@ -678,7 +679,8 @@ class DigDeeperStrategy(IStrategy):
# This is called when placing the initial order (opening trade) # This is called when placing the initial order (opening trade)
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, 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:
# We need to leave most of the funds for possible further DCA orders # We need to leave most of the funds for possible further DCA orders
# This also applies to fixed stakes # This also applies to fixed stakes

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -32,7 +32,8 @@ class Gateio(Exchange):
} }
_ft_has_futures: Dict = { _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]] = [ _supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [

View File

@ -28,6 +28,7 @@ class Okx(Exchange):
} }
_ft_has_futures: Dict = { _ft_has_futures: Dict = {
"tickers_have_quoteVolume": False, "tickers_have_quoteVolume": False,
"fee_cost_in_contracts": True,
} }
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [ _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, pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested,
time_in_force=time_in_force, current_time=datetime.now(timezone.utc), time_in_force=time_in_force, current_time=datetime.now(timezone.utc),
entry_tag=enter_tag, side=trade_side): 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 return False
order = self.exchange.create_order( order = self.exchange.create_order(
pair=pair, pair=pair,
@ -837,7 +837,7 @@ class FreqtradeBot(LoggingMixin):
pair=pair, current_time=datetime.now(timezone.utc), pair=pair, current_time=datetime.now(timezone.utc),
current_rate=enter_limit_requested, proposed_stake=stake_amount, current_rate=enter_limit_requested, proposed_stake=stake_amount,
min_stake=min_stake_amount, max_stake=min(max_stake_amount, stake_available), 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( 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) logger.debug(f'Found no {exit_signal_type} signal for %s.', trade)
return False 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: def create_stoploss_order(self, trade: Trade, stop_price: float) -> bool:
""" """
Abstracts creating stoploss orders from the logic. Abstracts creating stoploss orders from the logic.
@ -1137,28 +1160,6 @@ class FreqtradeBot(LoggingMixin):
logger.warning(f"Could not create trailing stoploss order " logger.warning(f"Could not create trailing stoploss order "
f"for pair {trade.pair}.") 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: def manage_open_orders(self) -> None:
""" """
Management of open orders on exchange. Unfilled orders might be cancelled if timeout 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, time_in_force=time_in_force, exit_reason=exit_reason,
sell_reason=exit_reason, # sellreason -> compatibility sell_reason=exit_reason, # sellreason -> compatibility
current_time=datetime.now(timezone.utc)): 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 return False
try: try:
@ -1795,7 +1796,8 @@ class FreqtradeBot(LoggingMixin):
trade_base_currency = self.exchange.get_pair_base_currency(trade.pair) trade_base_currency = self.exchange.get_pair_base_currency(trade.pair)
# use fee from order-dict if possible # use fee from order-dict if possible
if self.exchange.order_has_fee(order): 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}]: " logger.info(f"Fee for Trade {trade} [{order_obj.ft_order_side}]: "
f"{fee_cost:.8g} {fee_currency} - rate: {fee_rate}") f"{fee_cost:.8g} {fee_currency} - rate: {fee_rate}")
if fee_rate is None or fee_rate < 0.02: if fee_rate is None or fee_rate < 0.02:
@ -1833,7 +1835,15 @@ class FreqtradeBot(LoggingMixin):
for exectrade in trades: for exectrade in trades:
amount += exectrade['amount'] amount += exectrade['amount']
if self.exchange.order_has_fee(exectrade): 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_ fee_cost += fee_cost_
if fee_rate_ is not None: if fee_rate_ is not None:
fee_rate_array.append(fee_rate_) fee_rate_array.append(fee_rate_)

View File

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

View File

@ -1,9 +1,10 @@
import logging import logging
from typing import List from typing import List
from sqlalchemy import inspect, text from sqlalchemy import inspect, select, text, tuple_, update
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.persistence.trade_model import Order, Trade
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -252,31 +253,31 @@ def set_sqlite_to_wal(engine):
def fix_old_dry_orders(engine): def fix_old_dry_orders(engine):
with engine.begin() as connection: with engine.begin() as connection:
connection.execute( stmt = update(Order).where(
text( Order.ft_is_open.is_(True),
""" tuple_(Order.ft_trade_id, Order.order_id).not_in(
update orders select(
set ft_is_open = 0 Trade.id, Trade.stoploss_order_id
where ft_is_open = 1 and (ft_trade_id, order_id) not in ( ).where(Trade.stoploss_order_id.is_not(None))
select id, stoploss_order_id from trades where stoploss_order_id is not null ),
) and ft_order_side = 'stoploss' Order.ft_order_side == 'stoploss',
and order_id like 'dry_%' Order.order_id.like('dry%'),
"""
) ).values(ft_is_open=False)
) connection.execute(stmt)
connection.execute(
text( stmt = update(Order).where(
""" Order.ft_is_open.is_(True),
update orders tuple_(Order.ft_trade_id, Order.order_id).not_in(
set ft_is_open = 0 select(
where ft_is_open = 1 Trade.id, Trade.open_order_id
and (ft_trade_id, order_id) not in ( ).where(Trade.open_order_id.is_not(None))
select id, open_order_id from trades where open_order_id is not null ),
) and ft_order_side != 'stoploss' Order.ft_order_side != 'stoploss',
and order_id like 'dry_%' Order.order_id.like('dry%')
"""
) ).values(ft_is_open=False)
) connection.execute(stmt)
def check_migrate(engine, decl_base, previous_tables) -> None: 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.open_rate = total_stake / total_amount
self.stake_amount = total_stake / (self.leverage or 1.0) self.stake_amount = total_stake / (self.leverage or 1.0)
self.amount = total_amount 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() self.recalc_open_trade_value()
if self.stop_loss_pct is not None and self.open_rate is not None: 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) 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): and attr.category is not None and attr.category != category):
raise OperationalException( raise OperationalException(
f'Inconclusive parameter name {attr_name}, category: {attr.category}.') f'Inconclusive parameter name {attr_name}, category: {attr.category}.')
if (category == attr.category or if (category == attr.category or
(attr_name.startswith(category + '_') and attr.category is None)): (attr_name.startswith(category + '_') and attr.category is None)):
yield attr_name, attr 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, def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
proposed_stake: float, min_stake: Optional[float], max_stake: 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. 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 proposed_stake: A stake amount proposed by the bot.
:param min_stake: Minimal stake size allowed by exchange. :param min_stake: Minimal stake size allowed by exchange.
:param max_stake: Balance available for trading. :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 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 :param side: 'long' or 'short' - indicating the direction of the proposed trade
:return: A stake size, which is between min_stake and max_stake. :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 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, 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. 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 proposed_stake: A stake amount proposed by the bot.
:param min_stake: Minimal stake size allowed by exchange. :param min_stake: Minimal stake size allowed by exchange.
:param max_stake: Balance available for trading. :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 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 :param side: 'long' or 'short' - indicating the direction of the proposed trade
:return: A stake size, which is between min_stake and max_stake. :return: A stake size, which is between min_stake and max_stake.

View File

@ -8,22 +8,22 @@ coveralls==3.3.1
flake8==4.0.1 flake8==4.0.1
flake8-tidy-imports==4.8.0 flake8-tidy-imports==4.8.0
mypy==0.961 mypy==0.961
pre-commit==2.19.0 pre-commit==2.20.0
pytest==7.1.2 pytest==7.1.2
pytest-asyncio==0.18.3 pytest-asyncio==0.18.3
pytest-cov==3.0.0 pytest-cov==3.0.0
pytest-mock==3.7.0 pytest-mock==3.8.2
pytest-random-order==1.0.4 pytest-random-order==1.0.4
isort==5.10.1 isort==5.10.1
# For datetime mocking # For datetime mocking
time-machine==2.7.0 time-machine==2.7.1
# Convert jupyter notebooks to markdown documents # Convert jupyter notebooks to markdown documents
nbconvert==6.5.0 nbconvert==6.5.0
# mypy types # mypy types
types-cachetools==5.0.2 types-cachetools==5.2.1
types-filelock==3.2.7 types-filelock==3.2.7
types-requests==2.27.30 types-requests==2.28.0
types-tabulate==0.8.9 types-tabulate==0.8.11
types-python-dateutil==2.8.17 types-python-dateutil==2.8.18

View File

@ -1,4 +1,4 @@
# Include all requirements to run the bot. # Include all requirements to run the bot.
-r requirements.txt -r requirements.txt
plotly==5.8.2 plotly==5.9.0

View File

@ -1,21 +1,21 @@
numpy==1.23.0 numpy==1.23.1
pandas==1.4.2 pandas==1.4.3
pandas-ta==0.3.14b pandas-ta==0.3.14b
ccxt==1.88.15 ccxt==1.90.41
# Pin cryptography for now due to rust build errors with piwheels # Pin cryptography for now due to rust build errors with piwheels
cryptography==37.0.2 cryptography==37.0.4
aiohttp==3.8.1 aiohttp==3.8.1
SQLAlchemy==1.4.37 SQLAlchemy==1.4.39
python-telegram-bot==13.12 python-telegram-bot==13.13
arrow==1.2.2 arrow==1.2.2
cachetools==4.2.2 cachetools==4.2.2
requests==2.28.0 requests==2.28.1
urllib3==1.26.9 urllib3==1.26.10
jsonschema==4.6.0 jsonschema==4.6.2
TA-Lib==0.4.24 TA-Lib==0.4.24
technical==1.3.0 technical==1.3.0
tabulate==0.8.9 tabulate==0.8.10
pycoingecko==2.2.0 pycoingecko==2.2.0
jinja2==3.1.2 jinja2==3.1.2
tables==3.7.0 tables==3.7.0
@ -26,16 +26,16 @@ joblib==1.1.0
py_find_1st==1.1.5 py_find_1st==1.1.5
# Load ticker files 30% faster # Load ticker files 30% faster
python-rapidjson==1.6 python-rapidjson==1.8
# Properly format api responses # Properly format api responses
orjson==3.7.2 orjson==3.7.7
# Notify systemd # Notify systemd
sdnotify==0.3.2 sdnotify==0.3.2
# API Server # API Server
fastapi==0.78.0 fastapi==0.78.0
uvicorn==0.17.6 uvicorn==0.18.2
pyjwt==2.4.0 pyjwt==2.4.0
aiofiles==0.8.0 aiofiles==0.8.0
psutil==5.9.1 psutil==5.9.1
@ -44,7 +44,7 @@ psutil==5.9.1
colorama==0.4.5 colorama==0.4.5
# Building config files interactively # Building config files interactively
questionary==1.10.0 questionary==1.10.0
prompt-toolkit==3.0.29 prompt-toolkit==3.0.30
# Extensions to datetime library # Extensions to datetime library
python-dateutil==2.8.2 python-dateutil==2.8.2

View File

@ -1694,6 +1694,7 @@ def limit_buy_order_old_partial():
'price': 0.00001099, 'price': 0.00001099,
'amount': 90.99181073, 'amount': 90.99181073,
'filled': 23.0, 'filled': 23.0,
'cost': 90.99181073 * 23.0,
'remaining': 67.99181073, 'remaining': 67.99181073,
'status': 'open' 'status': 'open'
} }
@ -3166,60 +3167,46 @@ def leverage_tiers():
"AAVE/USDT": [ "AAVE/USDT": [
{ {
'min': 0, 'min': 0,
'max': 50000, 'max': 5000,
'mmr': 0.01, 'mmr': 0.01,
'lev': 50, 'lev': 50,
'maintAmt': 0.0 'maintAmt': 0.0
}, },
{ {
'min': 50000, 'min': 5000,
'max': 250000, 'max': 25000,
'mmr': 0.02, 'mmr': 0.02,
'lev': 25, 'lev': 25,
'maintAmt': 500.0 'maintAmt': 75.0
},
{
'min': 25000,
'max': 100000,
'mmr': 0.05,
'lev': 10,
'maintAmt': 700.0
},
{
'min': 100000,
'max': 250000,
'mmr': 0.1,
'lev': 5,
'maintAmt': 5700.0
}, },
{ {
'min': 250000, 'min': 250000,
'max': 1000000, 'max': 1000000,
'mmr': 0.05,
'lev': 10,
'maintAmt': 8000.0
},
{
'min': 1000000,
'max': 2000000,
'mmr': 0.1,
'lev': 5,
'maintAmt': 58000.0
},
{
'min': 2000000,
'max': 5000000,
'mmr': 0.125, 'mmr': 0.125,
'lev': 4, 'lev': 2,
'maintAmt': 108000.0 'maintAmt': 11950.0
},
{
'min': 5000000,
'max': 10000000,
'mmr': 0.1665,
'lev': 3,
'maintAmt': 315500.0
}, },
{ {
'min': 10000000, 'min': 10000000,
'max': 20000000, 'max': 50000000,
'mmr': 0.25, 'mmr': 0.5,
'lev': 2, 'lev': 1,
'maintAmt': 1150500.0 'maintAmt': 386950.0
}, },
{
"min": 20000000,
"max": 50000000,
"mmr": 0.5,
"lev": 1,
"maintAmt": 6150500.0
}
], ],
"ADA/BUSD": [ "ADA/BUSD": [
{ {

View File

@ -1078,9 +1078,10 @@ def test_validate_ordertypes(default_conf, mocker):
'stoploss': 'market', 'stoploss': 'market',
'stoploss_on_exchange': False 'stoploss_on_exchange': False
} }
with pytest.raises(OperationalException, # TODO: Revert once createMarketOrder is available again.
match=r'Exchange .* does not support market orders.'): # with pytest.raises(OperationalException,
Exchange(default_conf) # match=r'Exchange .* does not support market orders.'):
# Exchange(default_conf)
default_conf['order_types'] = { default_conf['order_types'] = {
'entry': 'limit', 'entry': 'limit',
@ -3627,7 +3628,7 @@ def test_order_has_fee(order, expected) -> None:
def test_extract_cost_curr_rate(mocker, default_conf, order, expected) -> None: def test_extract_cost_curr_rate(mocker, default_conf, order, expected) -> None:
mocker.patch('freqtrade.exchange.Exchange.calculate_fee_rate', MagicMock(return_value=0.01)) mocker.patch('freqtrade.exchange.Exchange.calculate_fee_rate', MagicMock(return_value=0.01))
ex = get_patched_exchange(mocker, default_conf) ex = get_patched_exchange(mocker, default_conf)
assert ex.extract_cost_curr_rate(order) == expected assert ex.extract_cost_curr_rate(order['fee'], order['symbol'], cost=20, amount=1) == expected
@pytest.mark.parametrize("order,unknown_fee_rate,expected", [ @pytest.mark.parametrize("order,unknown_fee_rate,expected", [
@ -3665,6 +3666,9 @@ def test_extract_cost_curr_rate(mocker, default_conf, order, expected) -> None:
'fee': {'currency': 'POINT', 'cost': 2.0, 'rate': None}}, 1, 4.0), 'fee': {'currency': 'POINT', 'cost': 2.0, 'rate': None}}, 1, 4.0),
({'symbol': 'POINT/BTC', 'amount': 0.04, 'cost': 0.5, ({'symbol': 'POINT/BTC', 'amount': 0.04, 'cost': 0.5,
'fee': {'currency': 'POINT', 'cost': 2.0, 'rate': None}}, 2, 8.0), 'fee': {'currency': 'POINT', 'cost': 2.0, 'rate': None}}, 2, 8.0),
# Missing currency
({'symbol': 'ETH/BTC', 'amount': 0.04, 'cost': 0.05,
'fee': {'currency': None, 'cost': 0.005}}, None, None),
]) ])
def test_calculate_fee_rate(mocker, default_conf, order, expected, unknown_fee_rate) -> None: def test_calculate_fee_rate(mocker, default_conf, order, expected, unknown_fee_rate) -> None:
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', return_value={'last': 0.081}) mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', return_value={'last': 0.081})
@ -3673,7 +3677,8 @@ def test_calculate_fee_rate(mocker, default_conf, order, expected, unknown_fee_r
ex = get_patched_exchange(mocker, default_conf) ex = get_patched_exchange(mocker, default_conf)
assert ex.calculate_fee_rate(order) == expected assert ex.calculate_fee_rate(order['fee'], order['symbol'],
cost=order['cost'], amount=order['amount']) == expected
@pytest.mark.parametrize('retrycount,max_retries,expected', [ @pytest.mark.parametrize('retrycount,max_retries,expected', [

View File

@ -861,6 +861,7 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None:
hyperopt.backtesting.exchange.get_max_leverage = MagicMock(return_value=1.0) hyperopt.backtesting.exchange.get_max_leverage = MagicMock(return_value=1.0)
assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto) assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto)
assert isinstance(hyperopt.backtesting.strategy.buy_rsi, IntParameter) assert isinstance(hyperopt.backtesting.strategy.buy_rsi, IntParameter)
assert hyperopt.backtesting.strategy.bot_loop_started is True
assert hyperopt.backtesting.strategy.buy_rsi.in_space is True assert hyperopt.backtesting.strategy.buy_rsi.in_space is True
assert hyperopt.backtesting.strategy.buy_rsi.value == 35 assert hyperopt.backtesting.strategy.buy_rsi.value == 35

View File

@ -44,6 +44,11 @@ class HyperoptableStrategy(StrategyTestV2):
}) })
return prot return prot
bot_loop_started = False
def bot_loop_start(self):
self.bot_loop_started = True
def bot_start(self, **kwargs) -> None: def bot_start(self, **kwargs) -> None:
""" """
Parameters can also be defined here ... Parameters can also be defined here ...

View File

@ -3965,9 +3965,9 @@ def test_ignore_roi_if_entry_signal(default_conf_usdt, limit_order, limit_order_
# Test if entry-signal is absent (should sell due to roi = true) # Test if entry-signal is absent (should sell due to roi = true)
if is_short: if is_short:
patch_get_signal(freqtrade, enter_long=False, exit_short=False) patch_get_signal(freqtrade, enter_long=False, exit_short=False, exit_tag='something')
else: else:
patch_get_signal(freqtrade, enter_long=False, exit_long=False) patch_get_signal(freqtrade, enter_long=False, exit_long=False, exit_tag='something')
assert freqtrade.handle_trade(trade) is True assert freqtrade.handle_trade(trade) is True
assert trade.exit_reason == ExitType.ROI.value assert trade.exit_reason == ExitType.ROI.value

View File

@ -1205,7 +1205,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
0.00258580, {stake}, {amount}, 0.00258580, {stake}, {amount},
'2019-11-28 12:44:24.000000', '2019-11-28 12:44:24.000000',
0.0, 0.0, 0.0, '5m', 0.0, 0.0, 0.0, '5m',
'buy_order', 'stop_order_id222') 'buy_order', 'dry_stop_order_id222')
""".format(fee=fee.return_value, """.format(fee=fee.return_value,
stake=default_conf.get("stake_amount"), stake=default_conf.get("stake_amount"),
amount=amount amount=amount
@ -1231,7 +1231,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
'buy', 'buy',
'ETC/BTC', 'ETC/BTC',
0, 0,
'buy_order', 'dry_buy_order',
'closed', 'closed',
'ETC/BTC', 'ETC/BTC',
'limit', 'limit',
@ -1241,14 +1241,46 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
{amount}, {amount},
0, 0,
{amount * 0.00258580} {amount * 0.00258580}
),
(
1,
'buy',
'ETC/BTC',
1,
'dry_buy_order22',
'canceled',
'ETC/BTC',
'limit',
'buy',
0.00258580,
{amount},
{amount},
0,
{amount * 0.00258580}
), ),
( (
1, 1,
'stoploss', 'stoploss',
'ETC/BTC', 'ETC/BTC',
1,
'dry_stop_order_id11X',
'canceled',
'ETC/BTC',
'limit',
'sell',
0.00258580,
{amount},
{amount},
0, 0,
'stop_order_id222', {amount * 0.00258580}
'closed', ),
(
1,
'stoploss',
'ETC/BTC',
1,
'dry_stop_order_id222',
'open',
'ETC/BTC', 'ETC/BTC',
'limit', 'limit',
'sell', 'sell',
@ -1297,7 +1329,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
assert trade.exit_reason is None assert trade.exit_reason is None
assert trade.strategy is None assert trade.strategy is None
assert trade.timeframe == '5m' assert trade.timeframe == '5m'
assert trade.stoploss_order_id == 'stop_order_id222' assert trade.stoploss_order_id == 'dry_stop_order_id222'
assert trade.stoploss_last_update is None assert trade.stoploss_last_update is None
assert log_has("trying trades_bak1", caplog) assert log_has("trying trades_bak1", caplog)
assert log_has("trying trades_bak2", caplog) assert log_has("trying trades_bak2", caplog)
@ -1307,12 +1339,21 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
assert trade.close_profit_abs is None assert trade.close_profit_abs is None
orders = trade.orders orders = trade.orders
assert len(orders) == 2 assert len(orders) == 4
assert orders[0].order_id == 'buy_order' assert orders[0].order_id == 'dry_buy_order'
assert orders[0].ft_order_side == 'buy' assert orders[0].ft_order_side == 'buy'
assert orders[1].order_id == 'stop_order_id222' assert orders[-1].order_id == 'dry_stop_order_id222'
assert orders[1].ft_order_side == 'stoploss' assert orders[-1].ft_order_side == 'stoploss'
assert orders[-1].ft_is_open is True
assert orders[1].order_id == 'dry_buy_order22'
assert orders[1].ft_order_side == 'buy'
assert orders[1].ft_is_open is False
assert orders[2].order_id == 'dry_stop_order_id11X'
assert orders[2].ft_order_side == 'stoploss'
assert orders[2].ft_is_open is False
def test_migrate_too_old(mocker, default_conf, fee, caplog): def test_migrate_too_old(mocker, default_conf, fee, caplog):