Merge branch 'develop' into pr/SurferAdmin/6916
This commit is contained in:
commit
ed03ef47ef
@ -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
|
||||||
|
@ -7,4 +7,5 @@ FROM freqtradeorg/freqtrade:develop
|
|||||||
# The below dependency - pyti - serves as an example. Please use whatever you need!
|
# The below dependency - pyti - serves as an example. Please use whatever you need!
|
||||||
RUN pip install --user pyti
|
RUN pip install --user pyti
|
||||||
|
|
||||||
|
# Switch back to user (only if you required root above)
|
||||||
# USER ftuser
|
# USER ftuser
|
||||||
|
@ -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).
|
||||||
|
@ -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)
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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).
|
||||||
|
@ -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()
|
||||||
@ -673,9 +674,10 @@ class DigDeeperStrategy(IStrategy):
|
|||||||
max_dca_multiplier = 5.5
|
max_dca_multiplier = 5.5
|
||||||
|
|
||||||
# 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
|
||||||
|
@ -24,7 +24,7 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None:
|
|||||||
|
|
||||||
print_colorized = config.get('print_colorized', False)
|
print_colorized = config.get('print_colorized', False)
|
||||||
print_json = config.get('print_json', False)
|
print_json = config.get('print_json', False)
|
||||||
export_csv = config.get('export_csv', None)
|
export_csv = config.get('export_csv')
|
||||||
no_details = config.get('hyperopt_list_no_details', False)
|
no_details = config.get('hyperopt_list_no_details', False)
|
||||||
no_header = False
|
no_header = False
|
||||||
|
|
||||||
|
@ -129,7 +129,7 @@ class Configuration:
|
|||||||
# Default to in-memory db for dry_run if not specified
|
# Default to in-memory db for dry_run if not specified
|
||||||
config['db_url'] = constants.DEFAULT_DB_DRYRUN_URL
|
config['db_url'] = constants.DEFAULT_DB_DRYRUN_URL
|
||||||
else:
|
else:
|
||||||
if not config.get('db_url', None):
|
if not config.get('db_url'):
|
||||||
config['db_url'] = constants.DEFAULT_DB_PROD_URL
|
config['db_url'] = constants.DEFAULT_DB_PROD_URL
|
||||||
logger.info('Dry run is disabled')
|
logger.info('Dry run is disabled')
|
||||||
|
|
||||||
@ -182,7 +182,7 @@ class Configuration:
|
|||||||
config['user_data_dir'] = create_userdata_dir(config['user_data_dir'], create_dir=False)
|
config['user_data_dir'] = create_userdata_dir(config['user_data_dir'], create_dir=False)
|
||||||
logger.info('Using user-data directory: %s ...', config['user_data_dir'])
|
logger.info('Using user-data directory: %s ...', config['user_data_dir'])
|
||||||
|
|
||||||
config.update({'datadir': create_datadir(config, self.args.get('datadir', None))})
|
config.update({'datadir': create_datadir(config, self.args.get('datadir'))})
|
||||||
logger.info('Using data directory: %s ...', config.get('datadir'))
|
logger.info('Using data directory: %s ...', config.get('datadir'))
|
||||||
|
|
||||||
if self.args.get('exportfilename'):
|
if self.args.get('exportfilename'):
|
||||||
@ -221,7 +221,7 @@ class Configuration:
|
|||||||
if config.get('max_open_trades') == -1:
|
if config.get('max_open_trades') == -1:
|
||||||
config['max_open_trades'] = float('inf')
|
config['max_open_trades'] = float('inf')
|
||||||
|
|
||||||
if self.args.get('stake_amount', None):
|
if self.args.get('stake_amount'):
|
||||||
# Convert explicitly to float to support CLI argument for both unlimited and value
|
# Convert explicitly to float to support CLI argument for both unlimited and value
|
||||||
try:
|
try:
|
||||||
self.args['stake_amount'] = float(self.args['stake_amount'])
|
self.args['stake_amount'] = float(self.args['stake_amount'])
|
||||||
@ -474,7 +474,7 @@ class Configuration:
|
|||||||
configuration instead of the content)
|
configuration instead of the content)
|
||||||
"""
|
"""
|
||||||
if (argname in self.args and self.args[argname] is not None
|
if (argname in self.args and self.args[argname] is not None
|
||||||
and self.args[argname] is not False):
|
and self.args[argname] is not False):
|
||||||
|
|
||||||
config.update({argname: self.args[argname]})
|
config.update({argname: self.args[argname]})
|
||||||
if logfun:
|
if logfun:
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -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
|
||||||
|
@ -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 = {}
|
||||||
@ -387,7 +388,7 @@ class Exchange:
|
|||||||
and market.get('base', None) is not None
|
and market.get('base', None) is not None
|
||||||
and (self.precisionMode != TICK_SIZE
|
and (self.precisionMode != TICK_SIZE
|
||||||
# Too low precision will falsify calculations
|
# Too low precision will falsify calculations
|
||||||
or market.get('precision', {}).get('price', None) > 1e-11)
|
or market.get('precision', {}).get('price') > 1e-11)
|
||||||
and ((self.trading_mode == TradingMode.SPOT and self.market_is_spot(market))
|
and ((self.trading_mode == TradingMode.SPOT and self.market_is_spot(market))
|
||||||
or (self.trading_mode == TradingMode.MARGIN and self.market_is_margin(market))
|
or (self.trading_mode == TradingMode.MARGIN and self.market_is_margin(market))
|
||||||
or (self.trading_mode == TradingMode.FUTURES and self.market_is_future(market)))
|
or (self.trading_mode == TradingMode.FUTURES and self.market_is_future(market)))
|
||||||
@ -537,7 +538,7 @@ class Exchange:
|
|||||||
# The internal info array is different for each particular market,
|
# The internal info array is different for each particular market,
|
||||||
# its contents depend on the exchange.
|
# its contents depend on the exchange.
|
||||||
# It can also be a string or similar ... so we need to verify that first.
|
# It can also be a string or similar ... so we need to verify that first.
|
||||||
elif (isinstance(self.markets[pair].get('info', None), dict)
|
elif (isinstance(self.markets[pair].get('info'), dict)
|
||||||
and self.markets[pair].get('info', {}).get('prohibitedIn', False)):
|
and self.markets[pair].get('info', {}).get('prohibitedIn', False)):
|
||||||
# Warn users about restricted pairs in whitelist.
|
# Warn users about restricted pairs in whitelist.
|
||||||
# We cannot determine reliably if Users are affected.
|
# We cannot determine reliably if Users are affected.
|
||||||
@ -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)):
|
||||||
@ -1631,27 +1635,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:
|
||||||
@ -1663,19 +1675,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
|
||||||
|
|
||||||
|
@ -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]] = [
|
||||||
|
@ -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]] = [
|
||||||
|
@ -67,7 +67,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config)
|
self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config)
|
||||||
|
|
||||||
init_db(self.config.get('db_url', None))
|
init_db(self.config['db_url'])
|
||||||
|
|
||||||
self.wallets = Wallets(self.config, self.exchange)
|
self.wallets = Wallets(self.config, self.exchange)
|
||||||
|
|
||||||
@ -634,7 +634,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,
|
||||||
@ -648,7 +648,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
)
|
)
|
||||||
order_obj = Order.parse_from_ccxt_object(order, pair, side)
|
order_obj = Order.parse_from_ccxt_object(order, pair, side)
|
||||||
order_id = order['id']
|
order_id = order['id']
|
||||||
order_status = order.get('status', None)
|
order_status = order.get('status')
|
||||||
logger.info(f"Order #{order_id} was created for {pair} and status is {order_status}.")
|
logger.info(f"Order #{order_id} was created for {pair} and status is {order_status}.")
|
||||||
|
|
||||||
# we assume the order is executed at the price requested
|
# we assume the order is executed at the price requested
|
||||||
@ -814,7 +814,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(
|
||||||
@ -969,6 +969,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.
|
||||||
@ -1120,28 +1143,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
|
||||||
@ -1474,7 +1475,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:
|
||||||
@ -1551,7 +1552,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
'open_date': trade.open_date,
|
'open_date': trade.open_date,
|
||||||
'close_date': trade.close_date or datetime.utcnow(),
|
'close_date': trade.close_date or datetime.utcnow(),
|
||||||
'stake_currency': self.config['stake_currency'],
|
'stake_currency': self.config['stake_currency'],
|
||||||
'fiat_currency': self.config.get('fiat_display_currency', None),
|
'fiat_currency': self.config.get('fiat_display_currency'),
|
||||||
}
|
}
|
||||||
|
|
||||||
if 'fiat_display_currency' in self.config:
|
if 'fiat_display_currency' in self.config:
|
||||||
@ -1672,7 +1673,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
if order['status'] in constants.NON_OPEN_EXCHANGE_STATES:
|
if order['status'] in constants.NON_OPEN_EXCHANGE_STATES:
|
||||||
# If a entry order was closed, force update on stoploss on exchange
|
# If a entry order was closed, force update on stoploss on exchange
|
||||||
if order.get('side', None) == trade.entry_side:
|
if order.get('side') == trade.entry_side:
|
||||||
trade = self.cancel_stoploss_on_exchange(trade)
|
trade = self.cancel_stoploss_on_exchange(trade)
|
||||||
# TODO: Margin will need to use interest_rate as well.
|
# TODO: Margin will need to use interest_rate as well.
|
||||||
# interest_rate = self.exchange.get_interest_rate()
|
# interest_rate = self.exchange.get_interest_rate()
|
||||||
@ -1761,7 +1762,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:
|
||||||
@ -1799,7 +1801,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_)
|
||||||
|
@ -87,7 +87,7 @@ class Backtesting:
|
|||||||
self.exchange = ExchangeResolver.load_exchange(self._exchange_name, self.config)
|
self.exchange = ExchangeResolver.load_exchange(self._exchange_name, self.config)
|
||||||
self.dataprovider = DataProvider(self.config, self.exchange)
|
self.dataprovider = DataProvider(self.config, self.exchange)
|
||||||
|
|
||||||
if self.config.get('strategy_list', None):
|
if self.config.get('strategy_list'):
|
||||||
for strat in list(self.config['strategy_list']):
|
for strat in list(self.config['strategy_list']):
|
||||||
stratconf = deepcopy(self.config)
|
stratconf = deepcopy(self.config)
|
||||||
stratconf['strategy'] = strat
|
stratconf['strategy'] = strat
|
||||||
@ -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):
|
||||||
@ -721,7 +722,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,
|
||||||
@ -1140,8 +1141,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.
|
||||||
|
@ -455,7 +455,7 @@ class Hyperopt:
|
|||||||
return self.opt.ask(n_points=n_points), [False for _ in range(n_points)]
|
return self.opt.ask(n_points=n_points), [False for _ in range(n_points)]
|
||||||
|
|
||||||
def start(self) -> None:
|
def start(self) -> None:
|
||||||
self.random_state = self._set_random_state(self.config.get('hyperopt_random_state', None))
|
self.random_state = self._set_random_state(self.config.get('hyperopt_random_state'))
|
||||||
logger.info(f"Using optimizer random state: {self.random_state}")
|
logger.info(f"Using optimizer random state: {self.random_state}")
|
||||||
self.hyperopt_table_header = -1
|
self.hyperopt_table_header = -1
|
||||||
# Initialize spaces ...
|
# Initialize spaces ...
|
||||||
|
@ -127,14 +127,14 @@ class HyperoptTools():
|
|||||||
'only_profitable': config.get('hyperopt_list_profitable', False),
|
'only_profitable': config.get('hyperopt_list_profitable', False),
|
||||||
'filter_min_trades': config.get('hyperopt_list_min_trades', 0),
|
'filter_min_trades': config.get('hyperopt_list_min_trades', 0),
|
||||||
'filter_max_trades': config.get('hyperopt_list_max_trades', 0),
|
'filter_max_trades': config.get('hyperopt_list_max_trades', 0),
|
||||||
'filter_min_avg_time': config.get('hyperopt_list_min_avg_time', None),
|
'filter_min_avg_time': config.get('hyperopt_list_min_avg_time'),
|
||||||
'filter_max_avg_time': config.get('hyperopt_list_max_avg_time', None),
|
'filter_max_avg_time': config.get('hyperopt_list_max_avg_time'),
|
||||||
'filter_min_avg_profit': config.get('hyperopt_list_min_avg_profit', None),
|
'filter_min_avg_profit': config.get('hyperopt_list_min_avg_profit'),
|
||||||
'filter_max_avg_profit': config.get('hyperopt_list_max_avg_profit', None),
|
'filter_max_avg_profit': config.get('hyperopt_list_max_avg_profit'),
|
||||||
'filter_min_total_profit': config.get('hyperopt_list_min_total_profit', None),
|
'filter_min_total_profit': config.get('hyperopt_list_min_total_profit'),
|
||||||
'filter_max_total_profit': config.get('hyperopt_list_max_total_profit', None),
|
'filter_max_total_profit': config.get('hyperopt_list_max_total_profit'),
|
||||||
'filter_min_objective': config.get('hyperopt_list_min_objective', None),
|
'filter_min_objective': config.get('hyperopt_list_min_objective'),
|
||||||
'filter_max_objective': config.get('hyperopt_list_max_objective', None),
|
'filter_max_objective': config.get('hyperopt_list_max_objective'),
|
||||||
}
|
}
|
||||||
if not HyperoptTools._test_hyperopt_results_exist(results_file):
|
if not HyperoptTools._test_hyperopt_results_exist(results_file):
|
||||||
# No file found.
|
# No file found.
|
||||||
|
@ -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__)
|
||||||
@ -251,31 +252,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:
|
||||||
|
@ -821,7 +821,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)
|
||||||
|
@ -30,7 +30,7 @@ class AgeFilter(IPairList):
|
|||||||
self._symbolsCheckFailed = PeriodicCache(maxsize=1000, ttl=86_400)
|
self._symbolsCheckFailed = PeriodicCache(maxsize=1000, ttl=86_400)
|
||||||
|
|
||||||
self._min_days_listed = pairlistconfig.get('min_days_listed', 10)
|
self._min_days_listed = pairlistconfig.get('min_days_listed', 10)
|
||||||
self._max_days_listed = pairlistconfig.get('max_days_listed', None)
|
self._max_days_listed = pairlistconfig.get('max_days_listed')
|
||||||
|
|
||||||
candle_limit = exchange.ohlcv_candle_limit('1d', self._config['candle_type_def'])
|
candle_limit = exchange.ohlcv_candle_limit('1d', self._config['candle_type_def'])
|
||||||
if self._min_days_listed < 1:
|
if self._min_days_listed < 1:
|
||||||
|
@ -21,7 +21,7 @@ class PerformanceFilter(IPairList):
|
|||||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||||
|
|
||||||
self._minutes = pairlistconfig.get('minutes', 0)
|
self._minutes = pairlistconfig.get('minutes', 0)
|
||||||
self._min_profit = pairlistconfig.get('min_profit', None)
|
self._min_profit = pairlistconfig.get('min_profit')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def needstickers(self) -> bool:
|
def needstickers(self) -> bool:
|
||||||
|
@ -27,7 +27,7 @@ class RangeStabilityFilter(IPairList):
|
|||||||
|
|
||||||
self._days = pairlistconfig.get('lookback_days', 10)
|
self._days = pairlistconfig.get('lookback_days', 10)
|
||||||
self._min_rate_of_change = pairlistconfig.get('min_rate_of_change', 0.01)
|
self._min_rate_of_change = pairlistconfig.get('min_rate_of_change', 0.01)
|
||||||
self._max_rate_of_change = pairlistconfig.get('max_rate_of_change', None)
|
self._max_rate_of_change = pairlistconfig.get('max_rate_of_change')
|
||||||
self._refresh_period = pairlistconfig.get('refresh_period', 1440)
|
self._refresh_period = pairlistconfig.get('refresh_period', 1440)
|
||||||
self._def_candletype = self._config['candle_type_def']
|
self._def_candletype = self._config['candle_type_def']
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ class PairListManager(LoggingMixin):
|
|||||||
self._blacklist = self._config['exchange'].get('pair_blacklist', [])
|
self._blacklist = self._config['exchange'].get('pair_blacklist', [])
|
||||||
self._pairlist_handlers: List[IPairList] = []
|
self._pairlist_handlers: List[IPairList] = []
|
||||||
self._tickers_needed = False
|
self._tickers_needed = False
|
||||||
for pairlist_handler_config in self._config.get('pairlists', None):
|
for pairlist_handler_config in self._config.get('pairlists', []):
|
||||||
pairlist_handler = PairListResolver.load_pairlist(
|
pairlist_handler = PairListResolver.load_pairlist(
|
||||||
pairlist_handler_config['method'],
|
pairlist_handler_config['method'],
|
||||||
exchange=exchange,
|
exchange=exchange,
|
||||||
|
@ -282,7 +282,7 @@ def get_strategy(strategy: str, config=Depends(get_config)):
|
|||||||
def list_available_pairs(timeframe: Optional[str] = None, stake_currency: Optional[str] = None,
|
def list_available_pairs(timeframe: Optional[str] = None, stake_currency: Optional[str] = None,
|
||||||
candletype: Optional[CandleType] = None, config=Depends(get_config)):
|
candletype: Optional[CandleType] = None, config=Depends(get_config)):
|
||||||
|
|
||||||
dh = get_datahandler(config['datadir'], config.get('dataformat_ohlcv', None))
|
dh = get_datahandler(config['datadir'], config.get('dataformat_ohlcv'))
|
||||||
trading_mode: TradingMode = config.get('trading_mode', TradingMode.SPOT)
|
trading_mode: TradingMode = config.get('trading_mode', TradingMode.SPOT)
|
||||||
pair_interval = dh.ohlcv_get_available_data(config['datadir'], trading_mode)
|
pair_interval = dh.ohlcv_get_available_data(config['datadir'], trading_mode)
|
||||||
|
|
||||||
|
@ -97,7 +97,7 @@ class RPC:
|
|||||||
"""
|
"""
|
||||||
self._freqtrade = freqtrade
|
self._freqtrade = freqtrade
|
||||||
self._config: Dict[str, Any] = freqtrade.config
|
self._config: Dict[str, Any] = freqtrade.config
|
||||||
if self._config.get('fiat_display_currency', None):
|
if self._config.get('fiat_display_currency'):
|
||||||
self._fiat_converter = CryptoToFiatConverter()
|
self._fiat_converter = CryptoToFiatConverter()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -566,7 +566,7 @@ class RPC:
|
|||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency)
|
pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency)
|
||||||
rate = tickers.get(pair, {}).get('last', None)
|
rate = tickers.get(pair, {}).get('last')
|
||||||
if rate:
|
if rate:
|
||||||
if pair.startswith(stake_currency) and not pair.endswith(stake_currency):
|
if pair.startswith(stake_currency) and not pair.endswith(stake_currency):
|
||||||
rate = 1.0 / rate
|
rate = 1.0 / rate
|
||||||
|
@ -261,7 +261,7 @@ class Telegram(RPCHandler):
|
|||||||
)
|
)
|
||||||
if msg.get('analyzed_candle'):
|
if msg.get('analyzed_candle'):
|
||||||
message += f"*Analyzed Candle:* `{msg['analyzed_candle']}`\n"
|
message += f"*Analyzed Candle:* `{msg['analyzed_candle']}`\n"
|
||||||
message += f"*Enter Tag:* `{msg['enter_tag']}`\n" if msg.get('enter_tag', None) else ""
|
message += f"*Enter Tag:* `{msg['enter_tag']}`\n" if msg.get('enter_tag') else ""
|
||||||
message += f"*Amount:* `{msg['amount']:.8f}`\n"
|
message += f"*Amount:* `{msg['amount']:.8f}`\n"
|
||||||
if msg.get('leverage') and msg.get('leverage', 1.0) != 1.0:
|
if msg.get('leverage') and msg.get('leverage', 1.0) != 1.0:
|
||||||
message += f"*Leverage:* `{msg['leverage']}`\n"
|
message += f"*Leverage:* `{msg['leverage']}`\n"
|
||||||
@ -274,7 +274,7 @@ class Telegram(RPCHandler):
|
|||||||
|
|
||||||
message += f"*Total:* `({round_coin_value(msg['stake_amount'], msg['stake_currency'])}"
|
message += f"*Total:* `({round_coin_value(msg['stake_amount'], msg['stake_currency'])}"
|
||||||
|
|
||||||
if msg.get('fiat_currency', None):
|
if msg.get('fiat_currency'):
|
||||||
message += f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}"
|
message += f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}"
|
||||||
|
|
||||||
message += ")`"
|
message += ")`"
|
||||||
@ -290,7 +290,7 @@ class Telegram(RPCHandler):
|
|||||||
msg['enter_tag'] = msg['enter_tag'] if "enter_tag" in msg.keys() else None
|
msg['enter_tag'] = msg['enter_tag'] if "enter_tag" in msg.keys() else None
|
||||||
msg['emoji'] = self._get_sell_emoji(msg)
|
msg['emoji'] = self._get_sell_emoji(msg)
|
||||||
msg['leverage_text'] = (f"*Leverage:* `{msg['leverage']:.1f}`\n"
|
msg['leverage_text'] = (f"*Leverage:* `{msg['leverage']:.1f}`\n"
|
||||||
if msg.get('leverage', None) and msg.get('leverage', 1.0) != 1.0
|
if msg.get('leverage') and msg.get('leverage', 1.0) != 1.0
|
||||||
else "")
|
else "")
|
||||||
|
|
||||||
# Check if all sell properties are available.
|
# Check if all sell properties are available.
|
||||||
|
@ -45,21 +45,21 @@ class Webhook(RPCHandler):
|
|||||||
try:
|
try:
|
||||||
whconfig = self._config['webhook']
|
whconfig = self._config['webhook']
|
||||||
if msg['type'] in [RPCMessageType.ENTRY]:
|
if msg['type'] in [RPCMessageType.ENTRY]:
|
||||||
valuedict = whconfig.get('webhookentry', None)
|
valuedict = whconfig.get('webhookentry')
|
||||||
elif msg['type'] in [RPCMessageType.ENTRY_CANCEL]:
|
elif msg['type'] in [RPCMessageType.ENTRY_CANCEL]:
|
||||||
valuedict = whconfig.get('webhookentrycancel', None)
|
valuedict = whconfig.get('webhookentrycancel')
|
||||||
elif msg['type'] in [RPCMessageType.ENTRY_FILL]:
|
elif msg['type'] in [RPCMessageType.ENTRY_FILL]:
|
||||||
valuedict = whconfig.get('webhookentryfill', None)
|
valuedict = whconfig.get('webhookentryfill')
|
||||||
elif msg['type'] == RPCMessageType.EXIT:
|
elif msg['type'] == RPCMessageType.EXIT:
|
||||||
valuedict = whconfig.get('webhookexit', None)
|
valuedict = whconfig.get('webhookexit')
|
||||||
elif msg['type'] == RPCMessageType.EXIT_FILL:
|
elif msg['type'] == RPCMessageType.EXIT_FILL:
|
||||||
valuedict = whconfig.get('webhookexitfill', None)
|
valuedict = whconfig.get('webhookexitfill')
|
||||||
elif msg['type'] == RPCMessageType.EXIT_CANCEL:
|
elif msg['type'] == RPCMessageType.EXIT_CANCEL:
|
||||||
valuedict = whconfig.get('webhookexitcancel', None)
|
valuedict = whconfig.get('webhookexitcancel')
|
||||||
elif msg['type'] in (RPCMessageType.STATUS,
|
elif msg['type'] in (RPCMessageType.STATUS,
|
||||||
RPCMessageType.STARTUP,
|
RPCMessageType.STARTUP,
|
||||||
RPCMessageType.WARNING):
|
RPCMessageType.WARNING):
|
||||||
valuedict = whconfig.get('webhookstatus', None)
|
valuedict = whconfig.get('webhookstatus')
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError('Unknown message type: {}'.format(msg['type']))
|
raise NotImplementedError('Unknown message type: {}'.format(msg['type']))
|
||||||
if not valuedict:
|
if not valuedict:
|
||||||
|
@ -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
|
||||||
|
@ -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.
|
||||||
|
@ -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.
|
||||||
|
@ -131,9 +131,9 @@ class Wallets:
|
|||||||
if isinstance(balances[currency], dict):
|
if isinstance(balances[currency], dict):
|
||||||
self._wallets[currency] = Wallet(
|
self._wallets[currency] = Wallet(
|
||||||
currency,
|
currency,
|
||||||
balances[currency].get('free', None),
|
balances[currency].get('free'),
|
||||||
balances[currency].get('used', None),
|
balances[currency].get('used'),
|
||||||
balances[currency].get('total', None)
|
balances[currency].get('total')
|
||||||
)
|
)
|
||||||
# Remove currencies no longer in get_balances output
|
# Remove currencies no longer in get_balances output
|
||||||
for currency in deepcopy(self._wallets):
|
for currency in deepcopy(self._wallets):
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -1,21 +1,21 @@
|
|||||||
numpy==1.22.4
|
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
|
||||||
|
|
||||||
|
@ -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'
|
||||||
}
|
}
|
||||||
@ -3165,60 +3166,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": [
|
||||||
{
|
{
|
||||||
|
@ -1027,9 +1027,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',
|
||||||
@ -3544,7 +3545,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", [
|
||||||
@ -3582,6 +3583,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})
|
||||||
@ -3590,7 +3594,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', [
|
||||||
|
@ -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
|
||||||
|
@ -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 ...
|
||||||
|
@ -3951,9 +3951,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
|
||||||
|
|
||||||
|
@ -1200,7 +1200,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
|
||||||
@ -1226,7 +1226,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',
|
||||||
@ -1238,12 +1238,44 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
|
|||||||
{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',
|
||||||
@ -1292,7 +1324,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)
|
||||||
@ -1302,12 +1334,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):
|
||||||
|
Loading…
Reference in New Issue
Block a user