Merge branch 'develop' into pr/mkavinkumar1/6545
This commit is contained in:
commit
0287b742da
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@ -351,7 +351,7 @@ jobs:
|
|||||||
python setup.py sdist bdist_wheel
|
python setup.py sdist bdist_wheel
|
||||||
|
|
||||||
- name: Publish to PyPI (Test)
|
- name: Publish to PyPI (Test)
|
||||||
uses: pypa/gh-action-pypi-publish@master
|
uses: pypa/gh-action-pypi-publish@v1.5.0
|
||||||
if: (github.event_name == 'release')
|
if: (github.event_name == 'release')
|
||||||
with:
|
with:
|
||||||
user: __token__
|
user: __token__
|
||||||
@ -359,7 +359,7 @@ jobs:
|
|||||||
repository_url: https://test.pypi.org/legacy/
|
repository_url: https://test.pypi.org/legacy/
|
||||||
|
|
||||||
- name: Publish to PyPI
|
- name: Publish to PyPI
|
||||||
uses: pypa/gh-action-pypi-publish@master
|
uses: pypa/gh-action-pypi-publish@v1.5.0
|
||||||
if: (github.event_name == 'release')
|
if: (github.event_name == 'release')
|
||||||
with:
|
with:
|
||||||
user: __token__
|
user: __token__
|
||||||
|
@ -15,7 +15,7 @@ repos:
|
|||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
- types-cachetools==5.2.1
|
- types-cachetools==5.2.1
|
||||||
- types-filelock==3.2.7
|
- types-filelock==3.2.7
|
||||||
- types-requests==2.28.0
|
- types-requests==2.28.1
|
||||||
- types-tabulate==0.8.11
|
- types-tabulate==0.8.11
|
||||||
- types-python-dateutil==2.8.18
|
- types-python-dateutil==2.8.18
|
||||||
# stages: [push]
|
# stages: [push]
|
||||||
|
@ -155,7 +155,8 @@
|
|||||||
"entry_cancel": "on",
|
"entry_cancel": "on",
|
||||||
"exit_cancel": "on",
|
"exit_cancel": "on",
|
||||||
"protection_trigger": "off",
|
"protection_trigger": "off",
|
||||||
"protection_trigger_global": "on"
|
"protection_trigger_global": "on",
|
||||||
|
"show_candle": "off"
|
||||||
},
|
},
|
||||||
"reload": true,
|
"reload": true,
|
||||||
"balance_dust_level": 0.01
|
"balance_dust_level": 0.01
|
||||||
|
@ -335,6 +335,7 @@ There are four parameter types each suited for different purposes.
|
|||||||
## Optimizing an indicator parameter
|
## Optimizing an indicator parameter
|
||||||
|
|
||||||
Assuming you have a simple strategy in mind - a EMA cross strategy (2 Moving averages crossing) - and you'd like to find the ideal parameters for this strategy.
|
Assuming you have a simple strategy in mind - a EMA cross strategy (2 Moving averages crossing) - and you'd like to find the ideal parameters for this strategy.
|
||||||
|
By default, we assume a stoploss of 5% - and a take-profit (`minimal_roi`) of 10% - which means freqtrade will sell the trade once 10% profit has been reached.
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
@ -349,6 +350,9 @@ import freqtrade.vendor.qtpylib.indicators as qtpylib
|
|||||||
class MyAwesomeStrategy(IStrategy):
|
class MyAwesomeStrategy(IStrategy):
|
||||||
stoploss = -0.05
|
stoploss = -0.05
|
||||||
timeframe = '15m'
|
timeframe = '15m'
|
||||||
|
minimal_roi = {
|
||||||
|
"0": 0.10
|
||||||
|
},
|
||||||
# Define the parameter spaces
|
# Define the parameter spaces
|
||||||
buy_ema_short = IntParameter(3, 50, default=5)
|
buy_ema_short = IntParameter(3, 50, default=5)
|
||||||
buy_ema_long = IntParameter(15, 200, default=50)
|
buy_ema_long = IntParameter(15, 200, default=50)
|
||||||
@ -383,7 +387,7 @@ class MyAwesomeStrategy(IStrategy):
|
|||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
conditions = []
|
conditions = []
|
||||||
conditions.append(qtpylib.crossed_above(
|
conditions.append(qtpylib.crossed_above(
|
||||||
dataframe[f'ema_long_{self.buy_ema_long.value}'], dataframe[f'ema_short_{self.buy_ema_short.value}']
|
dataframe[f'ema_long_{self.buy_ema_long.value}'], dataframe[f'ema_short_{self.buy_ema_short.value}']
|
||||||
))
|
))
|
||||||
@ -404,7 +408,7 @@ Using `self.buy_ema_short.range` will return a range object containing all entri
|
|||||||
In this case (`IntParameter(3, 50, default=5)`), the loop would run for all numbers between 3 and 50 (`[3, 4, 5, ... 49, 50]`).
|
In this case (`IntParameter(3, 50, default=5)`), the loop would run for all numbers between 3 and 50 (`[3, 4, 5, ... 49, 50]`).
|
||||||
By using this in a loop, hyperopt will generate 48 new columns (`['buy_ema_3', 'buy_ema_4', ... , 'buy_ema_50']`).
|
By using this in a loop, hyperopt will generate 48 new columns (`['buy_ema_3', 'buy_ema_4', ... , 'buy_ema_50']`).
|
||||||
|
|
||||||
Hyperopt itself will then use the selected value to create the buy and sell signals
|
Hyperopt itself will then use the selected value to create the buy and sell signals.
|
||||||
|
|
||||||
While this strategy is most likely too simple to provide consistent profit, it should serve as an example how optimize indicator parameters.
|
While this strategy is most likely too simple to provide consistent profit, it should serve as an example how optimize indicator parameters.
|
||||||
|
|
||||||
@ -868,6 +872,22 @@ To combat these, you have multiple options:
|
|||||||
* reduce the number of parallel processes (`-j <n>`)
|
* reduce the number of parallel processes (`-j <n>`)
|
||||||
* Increase the memory of your machine
|
* Increase the memory of your machine
|
||||||
|
|
||||||
|
## The objective has been evaluated at this point before.
|
||||||
|
|
||||||
|
If you see `The objective has been evaluated at this point before.` - then this is a sign that your space has been exhausted, or is close to that.
|
||||||
|
Basically all points in your space have been hit (or a local minima has been hit) - and hyperopt does no longer find points in the multi-dimensional space it did not try yet.
|
||||||
|
Freqtrade tries to counter the "local minima" problem by using new, randomized points in this case.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
``` python
|
||||||
|
buy_ema_short = IntParameter(5, 20, default=10, space="buy", optimize=True)
|
||||||
|
# This is the only parameter in the buy space
|
||||||
|
```
|
||||||
|
|
||||||
|
The `buy_ema_short` space has 15 possible values (`5, 6, ... 19, 20`). If you now run hyperopt for the buy space, hyperopt will only have 15 values to try before running out of options.
|
||||||
|
Your epochs should therefore be aligned to the possible values - or you should be ready to interrupt a run if you norice a lot of `The objective has been evaluated at this point before.` warnings.
|
||||||
|
|
||||||
## Show details of Hyperopt results
|
## Show details of Hyperopt results
|
||||||
|
|
||||||
After you run Hyperopt for the desired amount of epochs, you can later list all results for analysis, select only best or profitable once, and show the details for any of the epochs previously evaluated. This can be done with the `hyperopt-list` and `hyperopt-show` sub-commands. The usage of these sub-commands is described in the [Utils](utils.md#list-hyperopt-results) chapter.
|
After you run Hyperopt for the desired amount of epochs, you can later list all results for analysis, select only best or profitable once, and show the details for any of the epochs previously evaluated. This can be done with the `hyperopt-list` and `hyperopt-show` sub-commands. The usage of these sub-commands is described in the [Utils](utils.md#list-hyperopt-results) chapter.
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
markdown==3.3.7
|
||||||
mkdocs==1.3.0
|
mkdocs==1.3.0
|
||||||
mkdocs-material==8.3.9
|
mkdocs-material==8.3.9
|
||||||
mdx_truly_sane_lists==1.2
|
mdx_truly_sane_lists==1.2
|
||||||
|
@ -224,3 +224,5 @@ for val in self.buy_ema_short.range:
|
|||||||
# Append columns to existing dataframe
|
# Append columns to existing dataframe
|
||||||
merged_frame = pd.concat(frames, axis=1)
|
merged_frame = pd.concat(frames, axis=1)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Freqtrade does however also counter this by running `dataframe.copy()` on the dataframe right after the `populate_indicators()` method - so performance implications of this should be low to non-existant.
|
||||||
|
@ -31,11 +31,13 @@ pair = "BTC/USDT"
|
|||||||
```python
|
```python
|
||||||
# Load data using values set above
|
# Load data using values set above
|
||||||
from freqtrade.data.history import load_pair_history
|
from freqtrade.data.history import load_pair_history
|
||||||
|
from freqtrade.enums import CandleType
|
||||||
|
|
||||||
candles = load_pair_history(datadir=data_location,
|
candles = load_pair_history(datadir=data_location,
|
||||||
timeframe=config["timeframe"],
|
timeframe=config["timeframe"],
|
||||||
pair=pair,
|
pair=pair,
|
||||||
data_format = "hdf5",
|
data_format = "hdf5",
|
||||||
|
candle_type=CandleType.SPOT,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Confirm success
|
# Confirm success
|
||||||
@ -93,7 +95,7 @@ from freqtrade.data.btanalysis import load_backtest_data, load_backtest_stats
|
|||||||
|
|
||||||
# if backtest_dir points to a directory, it'll automatically load the last backtest file.
|
# if backtest_dir points to a directory, it'll automatically load the last backtest file.
|
||||||
backtest_dir = config["user_data_dir"] / "backtest_results"
|
backtest_dir = config["user_data_dir"] / "backtest_results"
|
||||||
# backtest_dir can also point to a specific file
|
# backtest_dir can also point to a specific file
|
||||||
# backtest_dir = config["user_data_dir"] / "backtest_results/backtest-result-2020-07-01_20-04-22.json"
|
# backtest_dir = config["user_data_dir"] / "backtest_results/backtest-result-2020-07-01_20-04-22.json"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -97,7 +97,8 @@ Example configuration showing the different settings:
|
|||||||
"entry_fill": "off",
|
"entry_fill": "off",
|
||||||
"exit_fill": "off",
|
"exit_fill": "off",
|
||||||
"protection_trigger": "off",
|
"protection_trigger": "off",
|
||||||
"protection_trigger_global": "on"
|
"protection_trigger_global": "on",
|
||||||
|
"show_candle": "off"
|
||||||
},
|
},
|
||||||
"reload": true,
|
"reload": true,
|
||||||
"balance_dust_level": 0.01
|
"balance_dust_level": 0.01
|
||||||
@ -108,7 +109,7 @@ Example configuration showing the different settings:
|
|||||||
`exit` notifications are sent when the order is placed, while `exit_fill` notifications are sent when the order is filled on the exchange.
|
`exit` notifications are sent when the order is placed, while `exit_fill` notifications are sent when the order is filled on the exchange.
|
||||||
`*_fill` notifications are off by default and must be explicitly enabled.
|
`*_fill` notifications are off by default and must be explicitly enabled.
|
||||||
`protection_trigger` notifications are sent when a protection triggers and `protection_trigger_global` notifications trigger when global protections are triggered.
|
`protection_trigger` notifications are sent when a protection triggers and `protection_trigger_global` notifications trigger when global protections are triggered.
|
||||||
|
`show_candle` - show candle values as part of entry/exit messages. Only possible value is "ohlc".
|
||||||
|
|
||||||
`balance_dust_level` will define what the `/balance` command takes as "dust" - Currencies with a balance below this will be shown.
|
`balance_dust_level` will define what the `/balance` command takes as "dust" - Currencies with a balance below this will be shown.
|
||||||
`reload` allows you to disable reload-buttons on selected messages.
|
`reload` allows you to disable reload-buttons on selected messages.
|
||||||
|
@ -313,6 +313,10 @@ CONF_SCHEMA = {
|
|||||||
'type': 'string',
|
'type': 'string',
|
||||||
'enum': TELEGRAM_SETTING_OPTIONS,
|
'enum': TELEGRAM_SETTING_OPTIONS,
|
||||||
},
|
},
|
||||||
|
'show_candle': {
|
||||||
|
'type': 'string',
|
||||||
|
'enum': ['off', 'ohlc'],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'reload': {'type': 'boolean'},
|
'reload': {'type': 'boolean'},
|
||||||
@ -538,3 +542,4 @@ TradeList = List[List]
|
|||||||
LongShort = Literal['long', 'short']
|
LongShort = Literal['long', 'short']
|
||||||
EntryExit = Literal['entry', 'exit']
|
EntryExit = Literal['entry', 'exit']
|
||||||
BuySell = Literal['buy', 'sell']
|
BuySell = Literal['buy', 'sell']
|
||||||
|
MakerTaker = Literal['maker', 'taker']
|
||||||
|
@ -73,7 +73,7 @@ EXCHANGE_HAS_REQUIRED = [
|
|||||||
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
|
'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
|
||||||
|
@ -20,7 +20,7 @@ from ccxt import ROUND_DOWN, ROUND_UP, TICK_SIZE, TRUNCATE, Precise, decimal_to_
|
|||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES, BuySell,
|
from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES, BuySell,
|
||||||
EntryExit, ListPairsWithTimeframes, PairWithTimeframe)
|
EntryExit, ListPairsWithTimeframes, MakerTaker, PairWithTimeframe)
|
||||||
from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list
|
from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list
|
||||||
from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, TradingMode
|
from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, TradingMode
|
||||||
from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError,
|
from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError,
|
||||||
@ -79,6 +79,7 @@ class Exchange:
|
|||||||
"ccxt_futures_name": "swap",
|
"ccxt_futures_name": "swap",
|
||||||
"fee_cost_in_contracts": False, # Fee cost needs contract conversion
|
"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
|
||||||
|
"order_props_in_contracts": ['amount', 'cost', 'filled', 'remaining'],
|
||||||
}
|
}
|
||||||
_ft_has: Dict = {}
|
_ft_has: Dict = {}
|
||||||
_ft_has_futures: Dict = {}
|
_ft_has_futures: Dict = {}
|
||||||
@ -175,23 +176,11 @@ class Exchange:
|
|||||||
logger.info(f'Using Exchange "{self.name}"')
|
logger.info(f'Using Exchange "{self.name}"')
|
||||||
|
|
||||||
if validate:
|
if validate:
|
||||||
# Check if timeframe is available
|
|
||||||
self.validate_timeframes(config.get('timeframe'))
|
|
||||||
|
|
||||||
# Initial markets load
|
# Initial markets load
|
||||||
self._load_markets()
|
self._load_markets()
|
||||||
|
self.validate_config(config)
|
||||||
# Check if all pairs are available
|
|
||||||
self.validate_stakecurrency(config['stake_currency'])
|
|
||||||
if not exchange_config.get('skip_pair_validation'):
|
|
||||||
self.validate_pairs(config['exchange']['pair_whitelist'])
|
|
||||||
self.validate_ordertypes(config.get('order_types', {}))
|
|
||||||
self.validate_order_time_in_force(config.get('order_time_in_force', {}))
|
|
||||||
self.required_candle_call_count = self.validate_required_startup_candles(
|
self.required_candle_call_count = self.validate_required_startup_candles(
|
||||||
config.get('startup_candle_count', 0), config.get('timeframe', ''))
|
config.get('startup_candle_count', 0), config.get('timeframe', ''))
|
||||||
self.validate_trading_mode_and_margin_mode(self.trading_mode, self.margin_mode)
|
|
||||||
self.validate_pricing(config['exit_pricing'])
|
|
||||||
self.validate_pricing(config['entry_pricing'])
|
|
||||||
|
|
||||||
# Converts the interval provided in minutes in config to seconds
|
# Converts the interval provided in minutes in config to seconds
|
||||||
self.markets_refresh_interval: int = exchange_config.get(
|
self.markets_refresh_interval: int = exchange_config.get(
|
||||||
@ -214,6 +203,20 @@ class Exchange:
|
|||||||
logger.info("Closing async ccxt session.")
|
logger.info("Closing async ccxt session.")
|
||||||
self.loop.run_until_complete(self._api_async.close())
|
self.loop.run_until_complete(self._api_async.close())
|
||||||
|
|
||||||
|
def validate_config(self, config):
|
||||||
|
# Check if timeframe is available
|
||||||
|
self.validate_timeframes(config.get('timeframe'))
|
||||||
|
|
||||||
|
# Check if all pairs are available
|
||||||
|
self.validate_stakecurrency(config['stake_currency'])
|
||||||
|
if not config['exchange'].get('skip_pair_validation'):
|
||||||
|
self.validate_pairs(config['exchange']['pair_whitelist'])
|
||||||
|
self.validate_ordertypes(config.get('order_types', {}))
|
||||||
|
self.validate_order_time_in_force(config.get('order_time_in_force', {}))
|
||||||
|
self.validate_trading_mode_and_margin_mode(self.trading_mode, self.margin_mode)
|
||||||
|
self.validate_pricing(config['exit_pricing'])
|
||||||
|
self.validate_pricing(config['entry_pricing'])
|
||||||
|
|
||||||
def _init_ccxt(self, exchange_config: Dict[str, Any], ccxt_module: CcxtModuleType = ccxt,
|
def _init_ccxt(self, exchange_config: Dict[str, Any], ccxt_module: CcxtModuleType = ccxt,
|
||||||
ccxt_kwargs: Dict = {}) -> ccxt.Exchange:
|
ccxt_kwargs: Dict = {}) -> ccxt.Exchange:
|
||||||
"""
|
"""
|
||||||
@ -423,7 +426,7 @@ class Exchange:
|
|||||||
if 'symbol' in order and order['symbol'] is not None:
|
if 'symbol' in order and order['symbol'] is not None:
|
||||||
contract_size = self._get_contract_size(order['symbol'])
|
contract_size = self._get_contract_size(order['symbol'])
|
||||||
if contract_size != 1:
|
if contract_size != 1:
|
||||||
for prop in ['amount', 'cost', 'filled', 'remaining']:
|
for prop in self._ft_has.get('order_props_in_contracts', []):
|
||||||
if prop in order and order[prop] is not None:
|
if prop in order and order[prop] is not None:
|
||||||
order[prop] = order[prop] * contract_size
|
order[prop] = order[prop] * contract_size
|
||||||
return order
|
return order
|
||||||
@ -586,13 +589,10 @@ class Exchange:
|
|||||||
"""
|
"""
|
||||||
Checks if order-types configured in strategy/config are supported
|
Checks if order-types configured in strategy/config are supported
|
||||||
"""
|
"""
|
||||||
# TODO: Reenable once ccxt fixes createMarketOrder assignment - as well as
|
if any(v == 'market' for k, v in order_types.items()):
|
||||||
# Revert the change in test_validate_ordertypes.
|
if not self.exchange_has('createMarketOrder'):
|
||||||
|
raise OperationalException(
|
||||||
# if any(v == 'market' for k, v in order_types.items()):
|
f'Exchange {self.name} does not support market orders.')
|
||||||
# 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)):
|
||||||
@ -824,7 +824,7 @@ class Exchange:
|
|||||||
'price': rate,
|
'price': rate,
|
||||||
'average': rate,
|
'average': rate,
|
||||||
'amount': _amount,
|
'amount': _amount,
|
||||||
'cost': _amount * rate / leverage,
|
'cost': _amount * rate,
|
||||||
'type': ordertype,
|
'type': ordertype,
|
||||||
'side': side,
|
'side': side,
|
||||||
'filled': 0,
|
'filled': 0,
|
||||||
@ -850,20 +850,27 @@ class Exchange:
|
|||||||
'filled': _amount,
|
'filled': _amount,
|
||||||
'cost': (dry_order['amount'] * average) / leverage
|
'cost': (dry_order['amount'] * average) / leverage
|
||||||
})
|
})
|
||||||
dry_order = self.add_dry_order_fee(pair, dry_order)
|
# market orders will always incurr taker fees
|
||||||
|
dry_order = self.add_dry_order_fee(pair, dry_order, 'taker')
|
||||||
|
|
||||||
dry_order = self.check_dry_limit_order_filled(dry_order)
|
dry_order = self.check_dry_limit_order_filled(dry_order, immediate=True)
|
||||||
|
|
||||||
self._dry_run_open_orders[dry_order["id"]] = dry_order
|
self._dry_run_open_orders[dry_order["id"]] = dry_order
|
||||||
# Copy order and close it - so the returned order is open unless it's a market order
|
# Copy order and close it - so the returned order is open unless it's a market order
|
||||||
return dry_order
|
return dry_order
|
||||||
|
|
||||||
def add_dry_order_fee(self, pair: str, dry_order: Dict[str, Any]) -> Dict[str, Any]:
|
def add_dry_order_fee(
|
||||||
|
self,
|
||||||
|
pair: str,
|
||||||
|
dry_order: Dict[str, Any],
|
||||||
|
taker_or_maker: MakerTaker,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
fee = self.get_fee(pair, taker_or_maker=taker_or_maker)
|
||||||
dry_order.update({
|
dry_order.update({
|
||||||
'fee': {
|
'fee': {
|
||||||
'currency': self.get_pair_quote_currency(pair),
|
'currency': self.get_pair_quote_currency(pair),
|
||||||
'cost': dry_order['cost'] * self.get_fee(pair),
|
'cost': dry_order['cost'] * fee,
|
||||||
'rate': self.get_fee(pair)
|
'rate': fee
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return dry_order
|
return dry_order
|
||||||
@ -929,7 +936,8 @@ class Exchange:
|
|||||||
pass
|
pass
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def check_dry_limit_order_filled(self, order: Dict[str, Any]) -> Dict[str, Any]:
|
def check_dry_limit_order_filled(
|
||||||
|
self, order: Dict[str, Any], immediate: bool = False) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Check dry-run limit order fill and update fee (if it filled).
|
Check dry-run limit order fill and update fee (if it filled).
|
||||||
"""
|
"""
|
||||||
@ -943,7 +951,12 @@ class Exchange:
|
|||||||
'filled': order['amount'],
|
'filled': order['amount'],
|
||||||
'remaining': 0,
|
'remaining': 0,
|
||||||
})
|
})
|
||||||
self.add_dry_order_fee(pair, order)
|
|
||||||
|
self.add_dry_order_fee(
|
||||||
|
pair,
|
||||||
|
order,
|
||||||
|
'taker' if immediate else 'maker',
|
||||||
|
)
|
||||||
|
|
||||||
return order
|
return order
|
||||||
|
|
||||||
@ -1631,7 +1644,7 @@ class Exchange:
|
|||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
def get_fee(self, symbol: str, type: str = '', side: str = '', amount: float = 1,
|
def get_fee(self, symbol: str, type: str = '', side: str = '', amount: float = 1,
|
||||||
price: float = 1, taker_or_maker: str = 'maker') -> float:
|
price: float = 1, taker_or_maker: MakerTaker = 'maker') -> float:
|
||||||
try:
|
try:
|
||||||
if self._config['dry_run'] and self._config.get('fee', None) is not None:
|
if self._config['dry_run'] and self._config.get('fee', None) is not None:
|
||||||
return self._config['fee']
|
return self._config['fee']
|
||||||
@ -1679,7 +1692,7 @@ class Exchange:
|
|||||||
fee_curr = fee.get('currency')
|
fee_curr = fee.get('currency')
|
||||||
if fee_curr is None:
|
if fee_curr is None:
|
||||||
return None
|
return None
|
||||||
fee_cost = fee['cost']
|
fee_cost = float(fee['cost'])
|
||||||
if self._ft_has['fee_cost_in_contracts']:
|
if self._ft_has['fee_cost_in_contracts']:
|
||||||
# Convert cost via "contracts" conversion
|
# Convert cost via "contracts" conversion
|
||||||
fee_cost = self._contracts_to_amount(symbol, fee['cost'])
|
fee_cost = self._contracts_to_amount(symbol, fee['cost'])
|
||||||
@ -1687,7 +1700,7 @@ class Exchange:
|
|||||||
# Calculate fee based on order details
|
# Calculate fee based on order details
|
||||||
if fee_curr == self.get_pair_base_currency(symbol):
|
if fee_curr == self.get_pair_base_currency(symbol):
|
||||||
# Base currency - divide by amount
|
# Base currency - divide by amount
|
||||||
return round(fee['cost'] / amount, 8)
|
return round(fee_cost / amount, 8)
|
||||||
elif fee_curr == self.get_pair_quote_currency(symbol):
|
elif fee_curr == self.get_pair_quote_currency(symbol):
|
||||||
# Quote currency - divide by cost
|
# Quote currency - divide by cost
|
||||||
return round(fee_cost / cost, 8) if cost else None
|
return round(fee_cost / cost, 8) if cost else None
|
||||||
@ -1718,7 +1731,7 @@ class Exchange:
|
|||||||
:param amount: Amount 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 (fee['cost'],
|
return (float(fee['cost']),
|
||||||
fee['currency'],
|
fee['currency'],
|
||||||
self.calculate_fee_rate(
|
self.calculate_fee_rate(
|
||||||
fee,
|
fee,
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
""" Gate.io exchange subclass """
|
""" Gate.io exchange subclass """
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
from freqtrade.constants import BuySell
|
from freqtrade.constants import BuySell
|
||||||
from freqtrade.enums import MarginMode, TradingMode
|
from freqtrade.enums import MarginMode, TradingMode
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
|
from freqtrade.misc import safe_value_fallback2
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -34,6 +35,7 @@ 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
|
"fee_cost_in_contracts": False, # Set explicitly to false for clarity
|
||||||
|
"order_props_in_contracts": ['amount', 'filled', 'remaining'],
|
||||||
}
|
}
|
||||||
|
|
||||||
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
|
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
|
||||||
@ -96,12 +98,29 @@ class Gateio(Exchange):
|
|||||||
}
|
}
|
||||||
return trades
|
return trades
|
||||||
|
|
||||||
|
def get_order_id_conditional(self, order: Dict[str, Any]) -> str:
|
||||||
|
if self.trading_mode == TradingMode.FUTURES:
|
||||||
|
return safe_value_fallback2(order, order, 'id_stop', 'id')
|
||||||
|
return order['id']
|
||||||
|
|
||||||
def fetch_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
|
def fetch_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
|
||||||
return self.fetch_order(
|
order = self.fetch_order(
|
||||||
order_id=order_id,
|
order_id=order_id,
|
||||||
pair=pair,
|
pair=pair,
|
||||||
params={'stop': True}
|
params={'stop': True}
|
||||||
)
|
)
|
||||||
|
if self.trading_mode == TradingMode.FUTURES:
|
||||||
|
if order['status'] == 'closed':
|
||||||
|
# Places a real order - which we need to fetch explicitly.
|
||||||
|
new_orderid = order.get('info', {}).get('trade_id')
|
||||||
|
if new_orderid:
|
||||||
|
order1 = self.fetch_order(order_id=new_orderid, pair=pair, params=params)
|
||||||
|
order1['id_stop'] = order1['id']
|
||||||
|
order1['id'] = order_id
|
||||||
|
order1['stopPrice'] = order.get('stopPrice')
|
||||||
|
|
||||||
|
return order1
|
||||||
|
return order
|
||||||
|
|
||||||
def cancel_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
|
def cancel_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
|
||||||
return self.cancel_order(
|
return self.cancel_order(
|
||||||
|
@ -333,6 +333,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
if not trade.is_open and not trade.fee_updated(trade.exit_side):
|
if not trade.is_open and not trade.fee_updated(trade.exit_side):
|
||||||
# Get sell fee
|
# Get sell fee
|
||||||
order = trade.select_order(trade.exit_side, False)
|
order = trade.select_order(trade.exit_side, False)
|
||||||
|
if not order:
|
||||||
|
order = trade.select_order('stoploss', False)
|
||||||
if order:
|
if order:
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Updating {trade.exit_side}-fee on trade {trade}"
|
f"Updating {trade.exit_side}-fee on trade {trade}"
|
||||||
|
@ -6,6 +6,7 @@ This module contains the hyperopt logic
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
|
import sys
|
||||||
import warnings
|
import warnings
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from math import ceil
|
from math import ceil
|
||||||
@ -17,6 +18,7 @@ import rapidjson
|
|||||||
from colorama import Fore, Style
|
from colorama import Fore, Style
|
||||||
from colorama import init as colorama_init
|
from colorama import init as colorama_init
|
||||||
from joblib import Parallel, cpu_count, delayed, dump, load, wrap_non_picklable_objects
|
from joblib import Parallel, cpu_count, delayed, dump, load, wrap_non_picklable_objects
|
||||||
|
from joblib.externals import cloudpickle
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, FTHYPT_FILEVERSION, LAST_BT_RESULT_FN
|
from freqtrade.constants import DATETIME_PRINT_FORMAT, FTHYPT_FILEVERSION, LAST_BT_RESULT_FN
|
||||||
@ -87,6 +89,7 @@ class Hyperopt:
|
|||||||
self.backtesting._set_strategy(self.backtesting.strategylist[0])
|
self.backtesting._set_strategy(self.backtesting.strategylist[0])
|
||||||
self.custom_hyperopt.strategy = self.backtesting.strategy
|
self.custom_hyperopt.strategy = self.backtesting.strategy
|
||||||
|
|
||||||
|
self.hyperopt_pickle_magic(self.backtesting.strategy.__class__.__bases__)
|
||||||
self.custom_hyperoptloss: IHyperOptLoss = HyperOptLossResolver.load_hyperoptloss(
|
self.custom_hyperoptloss: IHyperOptLoss = HyperOptLossResolver.load_hyperoptloss(
|
||||||
self.config)
|
self.config)
|
||||||
self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function
|
self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function
|
||||||
@ -137,6 +140,17 @@ class Hyperopt:
|
|||||||
logger.info(f"Removing `{p}`.")
|
logger.info(f"Removing `{p}`.")
|
||||||
p.unlink()
|
p.unlink()
|
||||||
|
|
||||||
|
def hyperopt_pickle_magic(self, bases) -> None:
|
||||||
|
"""
|
||||||
|
Hyperopt magic to allow strategy inheritance across files.
|
||||||
|
For this to properly work, we need to register the module of the imported class
|
||||||
|
to pickle as value.
|
||||||
|
"""
|
||||||
|
for modules in bases:
|
||||||
|
if modules.__name__ != 'IStrategy':
|
||||||
|
cloudpickle.register_pickle_by_value(sys.modules[modules.__module__])
|
||||||
|
self.hyperopt_pickle_magic(modules.__bases__)
|
||||||
|
|
||||||
def _get_params_dict(self, dimensions: List[Dimension], raw_params: List[Any]) -> Dict:
|
def _get_params_dict(self, dimensions: List[Dimension], raw_params: List[Any]) -> Dict:
|
||||||
|
|
||||||
# Ensure the number of dimensions match
|
# Ensure the number of dimensions match
|
||||||
|
@ -255,18 +255,18 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
|
|||||||
"""
|
"""
|
||||||
# Trades can be empty
|
# Trades can be empty
|
||||||
if trades is not None and len(trades) > 0:
|
if trades is not None and len(trades) > 0:
|
||||||
# Create description for sell summarizing the trade
|
# Create description for exit summarizing the trade
|
||||||
trades['desc'] = trades.apply(
|
trades['desc'] = trades.apply(
|
||||||
lambda row: f"{row['profit_ratio']:.2%}, " +
|
lambda row: f"{row['profit_ratio']:.2%}, " +
|
||||||
(f"{row['enter_tag']}, " if row['enter_tag'] is not None else "") +
|
(f"{row['enter_tag']}, " if row['enter_tag'] is not None else "") +
|
||||||
f"{row['exit_reason']}, " +
|
f"{row['exit_reason']}, " +
|
||||||
f"{row['trade_duration']} min",
|
f"{row['trade_duration']} min",
|
||||||
axis=1)
|
axis=1)
|
||||||
trade_buys = go.Scatter(
|
trade_entries = go.Scatter(
|
||||||
x=trades["open_date"],
|
x=trades["open_date"],
|
||||||
y=trades["open_rate"],
|
y=trades["open_rate"],
|
||||||
mode='markers',
|
mode='markers',
|
||||||
name='Trade buy',
|
name='Trade entry',
|
||||||
text=trades["desc"],
|
text=trades["desc"],
|
||||||
marker=dict(
|
marker=dict(
|
||||||
symbol='circle-open',
|
symbol='circle-open',
|
||||||
@ -277,12 +277,12 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
trade_sells = go.Scatter(
|
trade_exits = go.Scatter(
|
||||||
x=trades.loc[trades['profit_ratio'] > 0, "close_date"],
|
x=trades.loc[trades['profit_ratio'] > 0, "close_date"],
|
||||||
y=trades.loc[trades['profit_ratio'] > 0, "close_rate"],
|
y=trades.loc[trades['profit_ratio'] > 0, "close_rate"],
|
||||||
text=trades.loc[trades['profit_ratio'] > 0, "desc"],
|
text=trades.loc[trades['profit_ratio'] > 0, "desc"],
|
||||||
mode='markers',
|
mode='markers',
|
||||||
name='Sell - Profit',
|
name='Exit - Profit',
|
||||||
marker=dict(
|
marker=dict(
|
||||||
symbol='square-open',
|
symbol='square-open',
|
||||||
size=11,
|
size=11,
|
||||||
@ -290,12 +290,12 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
|
|||||||
color='green'
|
color='green'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
trade_sells_loss = go.Scatter(
|
trade_exits_loss = go.Scatter(
|
||||||
x=trades.loc[trades['profit_ratio'] <= 0, "close_date"],
|
x=trades.loc[trades['profit_ratio'] <= 0, "close_date"],
|
||||||
y=trades.loc[trades['profit_ratio'] <= 0, "close_rate"],
|
y=trades.loc[trades['profit_ratio'] <= 0, "close_rate"],
|
||||||
text=trades.loc[trades['profit_ratio'] <= 0, "desc"],
|
text=trades.loc[trades['profit_ratio'] <= 0, "desc"],
|
||||||
mode='markers',
|
mode='markers',
|
||||||
name='Sell - Loss',
|
name='Exit - Loss',
|
||||||
marker=dict(
|
marker=dict(
|
||||||
symbol='square-open',
|
symbol='square-open',
|
||||||
size=11,
|
size=11,
|
||||||
@ -303,9 +303,9 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
|
|||||||
color='red'
|
color='red'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
fig.add_trace(trade_buys, 1, 1)
|
fig.add_trace(trade_entries, 1, 1)
|
||||||
fig.add_trace(trade_sells, 1, 1)
|
fig.add_trace(trade_exits, 1, 1)
|
||||||
fig.add_trace(trade_sells_loss, 1, 1)
|
fig.add_trace(trade_exits_loss, 1, 1)
|
||||||
else:
|
else:
|
||||||
logger.warning("No trades found.")
|
logger.warning("No trades found.")
|
||||||
return fig
|
return fig
|
||||||
@ -444,7 +444,7 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra
|
|||||||
Generate the graph from the data generated by Backtesting or from DB
|
Generate the graph from the data generated by Backtesting or from DB
|
||||||
Volume will always be ploted in row2, so Row 1 and 3 are to our disposal for custom indicators
|
Volume will always be ploted in row2, so Row 1 and 3 are to our disposal for custom indicators
|
||||||
:param pair: Pair to Display on the graph
|
:param pair: Pair to Display on the graph
|
||||||
:param data: OHLCV DataFrame containing indicators and buy/sell signals
|
:param data: OHLCV DataFrame containing indicators and entry/exit signals
|
||||||
:param trades: All trades created
|
:param trades: All trades created
|
||||||
:param indicators1: List containing Main plot indicators
|
:param indicators1: List containing Main plot indicators
|
||||||
:param indicators2: List containing Sub plot indicators
|
:param indicators2: List containing Sub plot indicators
|
||||||
|
@ -243,6 +243,22 @@ class Telegram(RPCHandler):
|
|||||||
"""
|
"""
|
||||||
return f"{msg['exchange']}{' (dry)' if self._config['dry_run'] else ''}"
|
return f"{msg['exchange']}{' (dry)' if self._config['dry_run'] else ''}"
|
||||||
|
|
||||||
|
def _add_analyzed_candle(self, pair: str) -> str:
|
||||||
|
candle_val = self._config['telegram'].get(
|
||||||
|
'notification_settings', {}).get('show_candle', 'off')
|
||||||
|
if candle_val != 'off':
|
||||||
|
if candle_val == 'ohlc':
|
||||||
|
analyzed_df, _ = self._rpc._freqtrade.dataprovider.get_analyzed_dataframe(
|
||||||
|
pair, self._config['timeframe'])
|
||||||
|
candle = analyzed_df.iloc[-1].squeeze() if len(analyzed_df) > 0 else None
|
||||||
|
if candle is not None:
|
||||||
|
return (
|
||||||
|
f"*Candle OHLC*: `{candle['open']}, {candle['high']}, "
|
||||||
|
f"{candle['low']}, {candle['close']}`\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
return ''
|
||||||
|
|
||||||
def _format_entry_msg(self, msg: Dict[str, Any]) -> str:
|
def _format_entry_msg(self, msg: Dict[str, Any]) -> str:
|
||||||
if self._rpc._fiat_converter:
|
if self._rpc._fiat_converter:
|
||||||
msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount(
|
msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount(
|
||||||
@ -259,6 +275,7 @@ class Telegram(RPCHandler):
|
|||||||
f" {entry_side['entered'] if is_fill else entry_side['enter']} {msg['pair']}"
|
f" {entry_side['entered'] if is_fill else entry_side['enter']} {msg['pair']}"
|
||||||
f" (#{msg['trade_id']})\n"
|
f" (#{msg['trade_id']})\n"
|
||||||
)
|
)
|
||||||
|
message += self._add_analyzed_candle(msg['pair'])
|
||||||
message += f"*Enter Tag:* `{msg['enter_tag']}`\n" if msg.get('enter_tag') 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:
|
||||||
@ -322,6 +339,7 @@ class Telegram(RPCHandler):
|
|||||||
message = (
|
message = (
|
||||||
f"{msg['emoji']} *{self._exchange_from_msg(msg)}:* "
|
f"{msg['emoji']} *{self._exchange_from_msg(msg)}:* "
|
||||||
f"{'Exited' if is_fill else 'Exiting'} {msg['pair']} (#{msg['trade_id']})\n"
|
f"{'Exited' if is_fill else 'Exiting'} {msg['pair']} (#{msg['trade_id']})\n"
|
||||||
|
f"{self._add_analyzed_candle(msg['pair'])}"
|
||||||
f"*{f'{profit_prefix}Profit' if is_fill else f'Unrealized {profit_prefix}Profit'}:* "
|
f"*{f'{profit_prefix}Profit' if is_fill else f'Unrealized {profit_prefix}Profit'}:* "
|
||||||
f"`{msg['profit_ratio']:.2%}{msg['profit_extra']}`\n"
|
f"`{msg['profit_ratio']:.2%}{msg['profit_extra']}`\n"
|
||||||
f"{cp_extra}"
|
f"{cp_extra}"
|
||||||
|
@ -51,11 +51,13 @@
|
|||||||
"source": [
|
"source": [
|
||||||
"# Load data using values set above\n",
|
"# Load data using values set above\n",
|
||||||
"from freqtrade.data.history import load_pair_history\n",
|
"from freqtrade.data.history import load_pair_history\n",
|
||||||
|
"from freqtrade.enums import CandleType\n",
|
||||||
"\n",
|
"\n",
|
||||||
"candles = load_pair_history(datadir=data_location,\n",
|
"candles = load_pair_history(datadir=data_location,\n",
|
||||||
" timeframe=config[\"timeframe\"],\n",
|
" timeframe=config[\"timeframe\"],\n",
|
||||||
" pair=pair,\n",
|
" pair=pair,\n",
|
||||||
" data_format = \"hdf5\",\n",
|
" data_format = \"hdf5\",\n",
|
||||||
|
" candle_type=CandleType.SPOT,\n",
|
||||||
" )\n",
|
" )\n",
|
||||||
"\n",
|
"\n",
|
||||||
"# Confirm success\n",
|
"# Confirm success\n",
|
||||||
|
@ -10,7 +10,7 @@ flake8-tidy-imports==4.8.0
|
|||||||
mypy==0.961
|
mypy==0.961
|
||||||
pre-commit==2.20.0
|
pre-commit==2.20.0
|
||||||
pytest==7.1.2
|
pytest==7.1.2
|
||||||
pytest-asyncio==0.18.3
|
pytest-asyncio==0.19.0
|
||||||
pytest-cov==3.0.0
|
pytest-cov==3.0.0
|
||||||
pytest-mock==3.8.2
|
pytest-mock==3.8.2
|
||||||
pytest-random-order==1.0.4
|
pytest-random-order==1.0.4
|
||||||
@ -24,6 +24,6 @@ nbconvert==6.5.0
|
|||||||
# mypy types
|
# mypy types
|
||||||
types-cachetools==5.2.1
|
types-cachetools==5.2.1
|
||||||
types-filelock==3.2.7
|
types-filelock==3.2.7
|
||||||
types-requests==2.28.0
|
types-requests==2.28.1
|
||||||
types-tabulate==0.8.11
|
types-tabulate==0.8.11
|
||||||
types-python-dateutil==2.8.18
|
types-python-dateutil==2.8.18
|
||||||
|
@ -2,7 +2,7 @@ numpy==1.23.1
|
|||||||
pandas==1.4.3
|
pandas==1.4.3
|
||||||
pandas-ta==0.3.14b
|
pandas-ta==0.3.14b
|
||||||
|
|
||||||
ccxt==1.90.41
|
ccxt==1.90.89
|
||||||
# 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.4
|
cryptography==37.0.4
|
||||||
aiohttp==3.8.1
|
aiohttp==3.8.1
|
||||||
@ -12,7 +12,7 @@ arrow==1.2.2
|
|||||||
cachetools==4.2.2
|
cachetools==4.2.2
|
||||||
requests==2.28.1
|
requests==2.28.1
|
||||||
urllib3==1.26.10
|
urllib3==1.26.10
|
||||||
jsonschema==4.6.2
|
jsonschema==4.7.2
|
||||||
TA-Lib==0.4.24
|
TA-Lib==0.4.24
|
||||||
technical==1.3.0
|
technical==1.3.0
|
||||||
tabulate==0.8.10
|
tabulate==0.8.10
|
||||||
@ -34,7 +34,7 @@ orjson==3.7.7
|
|||||||
sdnotify==0.3.2
|
sdnotify==0.3.2
|
||||||
|
|
||||||
# API Server
|
# API Server
|
||||||
fastapi==0.78.0
|
fastapi==0.79.0
|
||||||
uvicorn==0.18.2
|
uvicorn==0.18.2
|
||||||
pyjwt==2.4.0
|
pyjwt==2.4.0
|
||||||
aiofiles==0.8.0
|
aiofiles==0.8.0
|
||||||
|
@ -112,11 +112,8 @@ def patch_exchange(
|
|||||||
mock_supported_modes=True
|
mock_supported_modes=True
|
||||||
) -> None:
|
) -> None:
|
||||||
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock(return_value={}))
|
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock(return_value={}))
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_config', MagicMock())
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_ordertypes', MagicMock())
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency', MagicMock())
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_pricing')
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.id', PropertyMock(return_value=id))
|
mocker.patch('freqtrade.exchange.Exchange.id', PropertyMock(return_value=id))
|
||||||
mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value=id.title()))
|
mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value=id.title()))
|
||||||
mocker.patch('freqtrade.exchange.Exchange.precisionMode', PropertyMock(return_value=2))
|
mocker.patch('freqtrade.exchange.Exchange.precisionMode', PropertyMock(return_value=2))
|
||||||
|
@ -153,6 +153,25 @@ class TestCCXTExchange():
|
|||||||
assert isinstance(markets[pair], dict)
|
assert isinstance(markets[pair], dict)
|
||||||
assert exchange.market_is_spot(markets[pair])
|
assert exchange.market_is_spot(markets[pair])
|
||||||
|
|
||||||
|
def test_has_validations(self, exchange):
|
||||||
|
|
||||||
|
exchange, exchangename = exchange
|
||||||
|
|
||||||
|
exchange.validate_ordertypes({
|
||||||
|
'entry': 'limit',
|
||||||
|
'exit': 'limit',
|
||||||
|
'stoploss': 'limit',
|
||||||
|
})
|
||||||
|
|
||||||
|
if exchangename == 'gateio':
|
||||||
|
# gateio doesn't have market orders on spot
|
||||||
|
return
|
||||||
|
exchange.validate_ordertypes({
|
||||||
|
'entry': 'market',
|
||||||
|
'exit': 'market',
|
||||||
|
'stoploss': 'market',
|
||||||
|
})
|
||||||
|
|
||||||
def test_load_markets_futures(self, exchange_futures):
|
def test_load_markets_futures(self, exchange_futures):
|
||||||
exchange, exchangename = exchange_futures
|
exchange, exchangename = exchange_futures
|
||||||
if not exchange:
|
if not exchange:
|
||||||
|
@ -1078,10 +1078,9 @@ def test_validate_ordertypes(default_conf, mocker):
|
|||||||
'stoploss': 'market',
|
'stoploss': 'market',
|
||||||
'stoploss_on_exchange': False
|
'stoploss_on_exchange': False
|
||||||
}
|
}
|
||||||
# TODO: Revert once createMarketOrder is available again.
|
with pytest.raises(OperationalException,
|
||||||
# with pytest.raises(OperationalException,
|
match=r'Exchange .* does not support market orders.'):
|
||||||
# match=r'Exchange .* does not support market orders.'):
|
Exchange(default_conf)
|
||||||
# Exchange(default_conf)
|
|
||||||
|
|
||||||
default_conf['order_types'] = {
|
default_conf['order_types'] = {
|
||||||
'entry': 'limit',
|
'entry': 'limit',
|
||||||
@ -1187,7 +1186,58 @@ def test_create_dry_run_order(default_conf, mocker, side, exchange_name, leverag
|
|||||||
assert order["symbol"] == "ETH/BTC"
|
assert order["symbol"] == "ETH/BTC"
|
||||||
assert order["amount"] == 1
|
assert order["amount"] == 1
|
||||||
assert order["leverage"] == leverage
|
assert order["leverage"] == leverage
|
||||||
assert order["cost"] == 1 * 200 / leverage
|
assert order["cost"] == 1 * 200
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('side,is_short,order_reason', [
|
||||||
|
("buy", False, "entry"),
|
||||||
|
("sell", False, "exit"),
|
||||||
|
("buy", True, "exit"),
|
||||||
|
("sell", True, "entry"),
|
||||||
|
])
|
||||||
|
@pytest.mark.parametrize("order_type,price_side,fee", [
|
||||||
|
("limit", "same", 1.0),
|
||||||
|
("limit", "other", 2.0),
|
||||||
|
("market", "same", 2.0),
|
||||||
|
("market", "other", 2.0),
|
||||||
|
])
|
||||||
|
def test_create_dry_run_order_fees(
|
||||||
|
default_conf,
|
||||||
|
mocker,
|
||||||
|
side,
|
||||||
|
order_type,
|
||||||
|
is_short,
|
||||||
|
order_reason,
|
||||||
|
price_side,
|
||||||
|
fee,
|
||||||
|
):
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.exchange.Exchange.get_fee',
|
||||||
|
side_effect=lambda symbol, taker_or_maker: 2.0 if taker_or_maker == 'taker' else 1.0
|
||||||
|
)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange._is_dry_limit_order_filled',
|
||||||
|
return_value=price_side == 'other')
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
|
|
||||||
|
order = exchange.create_dry_run_order(
|
||||||
|
pair='LTC/USDT',
|
||||||
|
ordertype=order_type,
|
||||||
|
side=side,
|
||||||
|
amount=10,
|
||||||
|
rate=2.0,
|
||||||
|
leverage=1.0
|
||||||
|
)
|
||||||
|
if price_side == 'other' or order_type == 'market':
|
||||||
|
assert order['fee']['rate'] == fee
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
assert order['fee'] is None
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange._is_dry_limit_order_filled',
|
||||||
|
return_value=price_side != 'other')
|
||||||
|
|
||||||
|
order1 = exchange.fetch_dry_run_order(order['id'])
|
||||||
|
assert order1['fee']['rate'] == fee
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("side,startprice,endprice", [
|
@pytest.mark.parametrize("side,startprice,endprice", [
|
||||||
|
@ -53,6 +53,25 @@ def test_fetch_stoploss_order_gateio(default_conf, mocker):
|
|||||||
assert fetch_order_mock.call_args_list[0][1]['pair'] == 'ETH/BTC'
|
assert fetch_order_mock.call_args_list[0][1]['pair'] == 'ETH/BTC'
|
||||||
assert fetch_order_mock.call_args_list[0][1]['params'] == {'stop': True}
|
assert fetch_order_mock.call_args_list[0][1]['params'] == {'stop': True}
|
||||||
|
|
||||||
|
default_conf['trading_mode'] = 'futures'
|
||||||
|
default_conf['margin_mode'] = 'isolated'
|
||||||
|
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, id='gateio')
|
||||||
|
|
||||||
|
exchange.fetch_order = MagicMock(return_value={
|
||||||
|
'status': 'closed',
|
||||||
|
'id': '1234',
|
||||||
|
'stopPrice': 5.62,
|
||||||
|
'info': {
|
||||||
|
'trade_id': '222555'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
exchange.fetch_stoploss_order('1234', 'ETH/BTC')
|
||||||
|
assert exchange.fetch_order.call_count == 2
|
||||||
|
assert exchange.fetch_order.call_args_list[0][1]['order_id'] == '1234'
|
||||||
|
assert exchange.fetch_order.call_args_list[1][1]['order_id'] == '222555'
|
||||||
|
|
||||||
|
|
||||||
def test_cancel_stoploss_order_gateio(default_conf, mocker):
|
def test_cancel_stoploss_order_gateio(default_conf, mocker):
|
||||||
exchange = get_patched_exchange(mocker, default_conf, id='gateio')
|
exchange = get_patched_exchange(mocker, default_conf, id='gateio')
|
||||||
|
@ -18,11 +18,11 @@ def hyperopt_conf(default_conf):
|
|||||||
'runmode': RunMode.HYPEROPT,
|
'runmode': RunMode.HYPEROPT,
|
||||||
'strategy': 'HyperoptableStrategy',
|
'strategy': 'HyperoptableStrategy',
|
||||||
'hyperopt_loss': 'ShortTradeDurHyperOptLoss',
|
'hyperopt_loss': 'ShortTradeDurHyperOptLoss',
|
||||||
'hyperopt_path': str(Path(__file__).parent / 'hyperopts'),
|
'hyperopt_path': str(Path(__file__).parent / 'hyperopts'),
|
||||||
'epochs': 1,
|
'epochs': 1,
|
||||||
'timerange': None,
|
'timerange': None,
|
||||||
'spaces': ['default'],
|
'spaces': ['default'],
|
||||||
'hyperopt_jobs': 1,
|
'hyperopt_jobs': 1,
|
||||||
'hyperopt_min_trades': 1,
|
'hyperopt_min_trades': 1,
|
||||||
})
|
})
|
||||||
return hyperconf
|
return hyperconf
|
||||||
|
@ -90,28 +90,6 @@ def load_data_test(what, testdatadir):
|
|||||||
fill_missing=True)}
|
fill_missing=True)}
|
||||||
|
|
||||||
|
|
||||||
def simple_backtest(config, contour, mocker, testdatadir) -> None:
|
|
||||||
patch_exchange(mocker)
|
|
||||||
config['timeframe'] = '1m'
|
|
||||||
backtesting = Backtesting(config)
|
|
||||||
backtesting._set_strategy(backtesting.strategylist[0])
|
|
||||||
|
|
||||||
data = load_data_test(contour, testdatadir)
|
|
||||||
processed = backtesting.strategy.advise_all_indicators(data)
|
|
||||||
min_date, max_date = get_timerange(processed)
|
|
||||||
assert isinstance(processed, dict)
|
|
||||||
results = backtesting.backtest(
|
|
||||||
processed=processed,
|
|
||||||
start_date=min_date,
|
|
||||||
end_date=max_date,
|
|
||||||
max_open_trades=1,
|
|
||||||
position_stacking=False,
|
|
||||||
enable_protections=config.get('enable_protections', False),
|
|
||||||
)
|
|
||||||
# results :: <class 'pandas.core.frame.DataFrame'>
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
# FIX: fixturize this?
|
# FIX: fixturize this?
|
||||||
def _make_backtest_conf(mocker, datadir, conf=None, pair='UNITTEST/BTC'):
|
def _make_backtest_conf(mocker, datadir, conf=None, pair='UNITTEST/BTC'):
|
||||||
data = history.load_data(datadir=datadir, timeframe='1m', pairs=[pair])
|
data = history.load_data(datadir=datadir, timeframe='1m', pairs=[pair])
|
||||||
@ -942,6 +920,7 @@ def test_backtest_dataprovider_analyzed_df(default_conf, fee, mocker, testdatadi
|
|||||||
def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatadir) -> None:
|
def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatadir) -> None:
|
||||||
# While this test IS a copy of test_backtest_pricecontours, it's needed to ensure
|
# While this test IS a copy of test_backtest_pricecontours, it's needed to ensure
|
||||||
# results do not carry-over to the next run, which is not given by using parametrize.
|
# results do not carry-over to the next run, which is not given by using parametrize.
|
||||||
|
patch_exchange(mocker)
|
||||||
default_conf['protections'] = [
|
default_conf['protections'] = [
|
||||||
{
|
{
|
||||||
"method": "CooldownPeriod",
|
"method": "CooldownPeriod",
|
||||||
@ -949,6 +928,7 @@ def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatad
|
|||||||
}]
|
}]
|
||||||
|
|
||||||
default_conf['enable_protections'] = True
|
default_conf['enable_protections'] = True
|
||||||
|
default_conf['timeframe'] = '1m'
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
|
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
|
||||||
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
|
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
|
||||||
@ -959,12 +939,27 @@ def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatad
|
|||||||
['sine', 9],
|
['sine', 9],
|
||||||
['raise', 10],
|
['raise', 10],
|
||||||
]
|
]
|
||||||
|
backtesting = Backtesting(default_conf)
|
||||||
|
backtesting._set_strategy(backtesting.strategylist[0])
|
||||||
|
|
||||||
# While entry-signals are unrealistic, running backtesting
|
# While entry-signals are unrealistic, running backtesting
|
||||||
# over and over again should not cause different results
|
# over and over again should not cause different results
|
||||||
for [contour, numres] in tests:
|
for [contour, numres] in tests:
|
||||||
# Debug output for random test failure
|
# Debug output for random test failure
|
||||||
print(f"{contour}, {numres}")
|
print(f"{contour}, {numres}")
|
||||||
assert len(simple_backtest(default_conf, contour, mocker, testdatadir)['results']) == numres
|
data = load_data_test(contour, testdatadir)
|
||||||
|
processed = backtesting.strategy.advise_all_indicators(data)
|
||||||
|
min_date, max_date = get_timerange(processed)
|
||||||
|
assert isinstance(processed, dict)
|
||||||
|
results = backtesting.backtest(
|
||||||
|
processed=processed,
|
||||||
|
start_date=min_date,
|
||||||
|
end_date=max_date,
|
||||||
|
max_open_trades=1,
|
||||||
|
position_stacking=False,
|
||||||
|
enable_protections=default_conf.get('enable_protections', False),
|
||||||
|
)
|
||||||
|
assert len(results['results']) == numres
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('protections,contour,expected', [
|
@pytest.mark.parametrize('protections,contour,expected', [
|
||||||
@ -990,7 +985,25 @@ def test_backtest_pricecontours(default_conf, fee, mocker, testdatadir,
|
|||||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
# While entry-signals are unrealistic, running backtesting
|
# While entry-signals are unrealistic, running backtesting
|
||||||
# over and over again should not cause different results
|
# over and over again should not cause different results
|
||||||
assert len(simple_backtest(default_conf, contour, mocker, testdatadir)['results']) == expected
|
|
||||||
|
patch_exchange(mocker)
|
||||||
|
default_conf['timeframe'] = '1m'
|
||||||
|
backtesting = Backtesting(default_conf)
|
||||||
|
backtesting._set_strategy(backtesting.strategylist[0])
|
||||||
|
|
||||||
|
data = load_data_test(contour, testdatadir)
|
||||||
|
processed = backtesting.strategy.advise_all_indicators(data)
|
||||||
|
min_date, max_date = get_timerange(processed)
|
||||||
|
assert isinstance(processed, dict)
|
||||||
|
results = backtesting.backtest(
|
||||||
|
processed=processed,
|
||||||
|
start_date=min_date,
|
||||||
|
end_date=max_date,
|
||||||
|
max_open_trades=1,
|
||||||
|
position_stacking=False,
|
||||||
|
enable_protections=default_conf.get('enable_protections', False),
|
||||||
|
)
|
||||||
|
assert len(results['results']) == expected
|
||||||
|
|
||||||
|
|
||||||
def test_backtest_clash_buy_sell(mocker, default_conf, testdatadir):
|
def test_backtest_clash_buy_sell(mocker, default_conf, testdatadir):
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# pragma pylint: disable=missing-docstring,W0212,C0103
|
# pragma pylint: disable=missing-docstring,W0212,C0103
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import ANY, MagicMock
|
from unittest.mock import ANY, MagicMock, PropertyMock
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import pytest
|
import pytest
|
||||||
@ -18,8 +18,8 @@ from freqtrade.optimize.hyperopt_tools import HyperoptTools
|
|||||||
from freqtrade.optimize.optimize_reports import generate_strategy_stats
|
from freqtrade.optimize.optimize_reports import generate_strategy_stats
|
||||||
from freqtrade.optimize.space import SKDecimal
|
from freqtrade.optimize.space import SKDecimal
|
||||||
from freqtrade.strategy import IntParameter
|
from freqtrade.strategy import IntParameter
|
||||||
from tests.conftest import (CURRENT_TEST_STRATEGY, get_args, log_has, log_has_re, patch_exchange,
|
from tests.conftest import (CURRENT_TEST_STRATEGY, get_args, get_markets, log_has, log_has_re,
|
||||||
patched_configuration_load_config_file)
|
patch_exchange, patched_configuration_load_config_file)
|
||||||
|
|
||||||
|
|
||||||
def generate_result_metrics():
|
def generate_result_metrics():
|
||||||
@ -855,7 +855,7 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None:
|
|||||||
'strategy': 'HyperoptableStrategy',
|
'strategy': 'HyperoptableStrategy',
|
||||||
'user_data_dir': Path(tmpdir),
|
'user_data_dir': Path(tmpdir),
|
||||||
'hyperopt_random_state': 42,
|
'hyperopt_random_state': 42,
|
||||||
'spaces': ['all']
|
'spaces': ['all'],
|
||||||
})
|
})
|
||||||
hyperopt = Hyperopt(hyperopt_conf)
|
hyperopt = Hyperopt(hyperopt_conf)
|
||||||
hyperopt.backtesting.exchange.get_max_leverage = MagicMock(return_value=1.0)
|
hyperopt.backtesting.exchange.get_max_leverage = MagicMock(return_value=1.0)
|
||||||
@ -883,6 +883,45 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None:
|
|||||||
hyperopt.get_optimizer([], 2)
|
hyperopt.get_optimizer([], 2)
|
||||||
|
|
||||||
|
|
||||||
|
def test_in_strategy_auto_hyperopt_with_parallel(mocker, hyperopt_conf, tmpdir, fee) -> None:
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_config', MagicMock())
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange._load_markets')
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.markets',
|
||||||
|
PropertyMock(return_value=get_markets()))
|
||||||
|
(Path(tmpdir) / 'hyperopt_results').mkdir(parents=True)
|
||||||
|
# No hyperopt needed
|
||||||
|
hyperopt_conf.update({
|
||||||
|
'strategy': 'HyperoptableStrategy',
|
||||||
|
'user_data_dir': Path(tmpdir),
|
||||||
|
'hyperopt_random_state': 42,
|
||||||
|
'spaces': ['all'],
|
||||||
|
# Enforce parallelity
|
||||||
|
'epochs': 2,
|
||||||
|
'hyperopt_jobs': 2,
|
||||||
|
'fee': fee.return_value,
|
||||||
|
})
|
||||||
|
hyperopt = Hyperopt(hyperopt_conf)
|
||||||
|
hyperopt.backtesting.exchange.get_max_leverage = lambda *x, **xx: 1.0
|
||||||
|
hyperopt.backtesting.exchange.get_min_pair_stake_amount = lambda *x, **xx: 1.0
|
||||||
|
hyperopt.backtesting.exchange.get_max_pair_stake_amount = lambda *x, **xx: 100.0
|
||||||
|
|
||||||
|
assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto)
|
||||||
|
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.value == 35
|
||||||
|
assert hyperopt.backtesting.strategy.sell_rsi.value == 74
|
||||||
|
assert hyperopt.backtesting.strategy.protection_cooldown_lookback.value == 30
|
||||||
|
buy_rsi_range = hyperopt.backtesting.strategy.buy_rsi.range
|
||||||
|
assert isinstance(buy_rsi_range, range)
|
||||||
|
# Range from 0 - 50 (inclusive)
|
||||||
|
assert len(list(buy_rsi_range)) == 51
|
||||||
|
|
||||||
|
hyperopt.start()
|
||||||
|
|
||||||
|
|
||||||
def test_SKDecimal():
|
def test_SKDecimal():
|
||||||
space = SKDecimal(1, 2, decimals=2)
|
space = SKDecimal(1, 2, decimals=2)
|
||||||
assert 1.5 in space
|
assert 1.5 in space
|
||||||
|
@ -1398,6 +1398,7 @@ def test_api_strategies(botclient):
|
|||||||
|
|
||||||
assert rc.json() == {'strategies': [
|
assert rc.json() == {'strategies': [
|
||||||
'HyperoptableStrategy',
|
'HyperoptableStrategy',
|
||||||
|
'HyperoptableStrategyV2',
|
||||||
'InformativeDecoratorTest',
|
'InformativeDecoratorTest',
|
||||||
'StrategyTestV2',
|
'StrategyTestV2',
|
||||||
'StrategyTestV3',
|
'StrategyTestV3',
|
||||||
|
@ -12,6 +12,7 @@ from unittest.mock import ANY, MagicMock
|
|||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import pytest
|
import pytest
|
||||||
|
from pandas import DataFrame
|
||||||
from telegram import Chat, Message, ReplyKeyboardMarkup, Update
|
from telegram import Chat, Message, ReplyKeyboardMarkup, Update
|
||||||
from telegram.error import BadRequest, NetworkError, TelegramError
|
from telegram.error import BadRequest, NetworkError, TelegramError
|
||||||
|
|
||||||
@ -1661,8 +1662,17 @@ def test_show_config_handle(default_conf, update, mocker) -> None:
|
|||||||
(RPCMessageType.ENTRY, 'Long', 'long_signal_01', 1.0),
|
(RPCMessageType.ENTRY, 'Long', 'long_signal_01', 1.0),
|
||||||
(RPCMessageType.ENTRY, 'Long', 'long_signal_01', 5.0),
|
(RPCMessageType.ENTRY, 'Long', 'long_signal_01', 5.0),
|
||||||
(RPCMessageType.ENTRY, 'Short', 'short_signal_01', 2.0)])
|
(RPCMessageType.ENTRY, 'Short', 'short_signal_01', 2.0)])
|
||||||
def test_send_msg_buy_notification(default_conf, mocker, caplog, message_type,
|
def test_send_msg_enter_notification(default_conf, mocker, caplog, message_type,
|
||||||
enter, enter_signal, leverage) -> None:
|
enter, enter_signal, leverage) -> None:
|
||||||
|
default_conf['telegram']['notification_settings']['show_candle'] = 'ohlc'
|
||||||
|
df = DataFrame({
|
||||||
|
'open': [1.1],
|
||||||
|
'high': [2.2],
|
||||||
|
'low': [1.0],
|
||||||
|
'close': [1.5],
|
||||||
|
})
|
||||||
|
mocker.patch('freqtrade.data.dataprovider.DataProvider.get_analyzed_dataframe',
|
||||||
|
return_value=(df, 1))
|
||||||
|
|
||||||
msg = {
|
msg = {
|
||||||
'type': message_type,
|
'type': message_type,
|
||||||
@ -1680,6 +1690,7 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog, message_type,
|
|||||||
'fiat_currency': 'USD',
|
'fiat_currency': 'USD',
|
||||||
'current_rate': 1.099e-05,
|
'current_rate': 1.099e-05,
|
||||||
'amount': 1333.3333333333335,
|
'amount': 1333.3333333333335,
|
||||||
|
'analyzed_candle': {'open': 1.1, 'high': 2.2, 'low': 1.0, 'close': 1.5},
|
||||||
'open_date': arrow.utcnow().shift(hours=-1)
|
'open_date': arrow.utcnow().shift(hours=-1)
|
||||||
}
|
}
|
||||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||||
@ -1689,6 +1700,7 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog, message_type,
|
|||||||
|
|
||||||
assert msg_mock.call_args[0][0] == (
|
assert msg_mock.call_args[0][0] == (
|
||||||
f'\N{LARGE BLUE CIRCLE} *Binance (dry):* {enter} ETH/BTC (#1)\n'
|
f'\N{LARGE BLUE CIRCLE} *Binance (dry):* {enter} ETH/BTC (#1)\n'
|
||||||
|
'*Candle OHLC*: `1.1, 2.2, 1.0, 1.5`\n'
|
||||||
f'*Enter Tag:* `{enter_signal}`\n'
|
f'*Enter Tag:* `{enter_signal}`\n'
|
||||||
'*Amount:* `1333.33333333`\n'
|
'*Amount:* `1333.33333333`\n'
|
||||||
f'{leverage_text}'
|
f'{leverage_text}'
|
||||||
@ -1716,7 +1728,8 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog, message_type,
|
|||||||
@pytest.mark.parametrize('message_type,enter_signal', [
|
@pytest.mark.parametrize('message_type,enter_signal', [
|
||||||
(RPCMessageType.ENTRY_CANCEL, 'long_signal_01'),
|
(RPCMessageType.ENTRY_CANCEL, 'long_signal_01'),
|
||||||
(RPCMessageType.ENTRY_CANCEL, 'short_signal_01')])
|
(RPCMessageType.ENTRY_CANCEL, 'short_signal_01')])
|
||||||
def test_send_msg_buy_cancel_notification(default_conf, mocker, message_type, enter_signal) -> None:
|
def test_send_msg_enter_cancel_notification(
|
||||||
|
default_conf, mocker, message_type, enter_signal) -> None:
|
||||||
|
|
||||||
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
|
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||||
|
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
|
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
|
||||||
|
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
from strategy_test_v2 import StrategyTestV2
|
from strategy_test_v3 import StrategyTestV3
|
||||||
|
|
||||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||||
from freqtrade.strategy import BooleanParameter, DecimalParameter, IntParameter, RealParameter
|
from freqtrade.strategy import BooleanParameter, DecimalParameter, IntParameter, RealParameter
|
||||||
|
|
||||||
|
|
||||||
class HyperoptableStrategy(StrategyTestV2):
|
class HyperoptableStrategy(StrategyTestV3):
|
||||||
"""
|
"""
|
||||||
Default Strategy provided by freqtrade bot.
|
Default Strategy provided by freqtrade bot.
|
||||||
Please do not modify this strategy, it's intended for internal use only.
|
Please do not modify this strategy, it's intended for internal use only.
|
||||||
|
54
tests/strategy/strats/hyperoptable_strategy_v2.py
Normal file
54
tests/strategy/strats/hyperoptable_strategy_v2.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
|
||||||
|
|
||||||
|
from strategy_test_v2 import StrategyTestV2
|
||||||
|
|
||||||
|
from freqtrade.strategy import BooleanParameter, DecimalParameter, IntParameter, RealParameter
|
||||||
|
|
||||||
|
|
||||||
|
class HyperoptableStrategyV2(StrategyTestV2):
|
||||||
|
"""
|
||||||
|
Default Strategy provided by freqtrade bot.
|
||||||
|
Please do not modify this strategy, it's intended for internal use only.
|
||||||
|
Please look at the SampleStrategy in the user_data/strategy directory
|
||||||
|
or strategy repository https://github.com/freqtrade/freqtrade-strategies
|
||||||
|
for samples and inspiration.
|
||||||
|
"""
|
||||||
|
|
||||||
|
buy_params = {
|
||||||
|
'buy_rsi': 35,
|
||||||
|
# Intentionally not specified, so "default" is tested
|
||||||
|
# 'buy_plusdi': 0.4
|
||||||
|
}
|
||||||
|
|
||||||
|
sell_params = {
|
||||||
|
'sell_rsi': 74,
|
||||||
|
'sell_minusdi': 0.4
|
||||||
|
}
|
||||||
|
|
||||||
|
buy_plusdi = RealParameter(low=0, high=1, default=0.5, space='buy')
|
||||||
|
sell_rsi = IntParameter(low=50, high=100, default=70, space='sell')
|
||||||
|
sell_minusdi = DecimalParameter(low=0, high=1, default=0.5001, decimals=3, space='sell',
|
||||||
|
load=False)
|
||||||
|
protection_enabled = BooleanParameter(default=True)
|
||||||
|
protection_cooldown_lookback = IntParameter([0, 50], default=30)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def protections(self):
|
||||||
|
prot = []
|
||||||
|
if self.protection_enabled.value:
|
||||||
|
prot.append({
|
||||||
|
"method": "CooldownPeriod",
|
||||||
|
"stop_duration_candles": self.protection_cooldown_lookback.value
|
||||||
|
})
|
||||||
|
return prot
|
||||||
|
|
||||||
|
bot_loop_started = False
|
||||||
|
|
||||||
|
def bot_loop_start(self):
|
||||||
|
self.bot_loop_started = True
|
||||||
|
|
||||||
|
def bot_start(self, **kwargs) -> None:
|
||||||
|
"""
|
||||||
|
Parameters can also be defined here ...
|
||||||
|
"""
|
||||||
|
self.buy_rsi = IntParameter([0, 50], default=30, space='buy')
|
@ -916,7 +916,7 @@ def test_hyperopt_parameters():
|
|||||||
|
|
||||||
|
|
||||||
def test_auto_hyperopt_interface(default_conf):
|
def test_auto_hyperopt_interface(default_conf):
|
||||||
default_conf.update({'strategy': 'HyperoptableStrategy'})
|
default_conf.update({'strategy': 'HyperoptableStrategyV2'})
|
||||||
PairLocks.timeframe = default_conf['timeframe']
|
PairLocks.timeframe = default_conf['timeframe']
|
||||||
strategy = StrategyResolver.load_strategy(default_conf)
|
strategy = StrategyResolver.load_strategy(default_conf)
|
||||||
strategy.ft_bot_start()
|
strategy.ft_bot_start()
|
||||||
|
@ -34,7 +34,7 @@ def test_search_all_strategies_no_failed():
|
|||||||
directory = Path(__file__).parent / "strats"
|
directory = Path(__file__).parent / "strats"
|
||||||
strategies = StrategyResolver.search_all_objects(directory, enum_failed=False)
|
strategies = StrategyResolver.search_all_objects(directory, enum_failed=False)
|
||||||
assert isinstance(strategies, list)
|
assert isinstance(strategies, list)
|
||||||
assert len(strategies) == 6
|
assert len(strategies) == 7
|
||||||
assert isinstance(strategies[0], dict)
|
assert isinstance(strategies[0], dict)
|
||||||
|
|
||||||
|
|
||||||
@ -42,10 +42,10 @@ def test_search_all_strategies_with_failed():
|
|||||||
directory = Path(__file__).parent / "strats"
|
directory = Path(__file__).parent / "strats"
|
||||||
strategies = StrategyResolver.search_all_objects(directory, enum_failed=True)
|
strategies = StrategyResolver.search_all_objects(directory, enum_failed=True)
|
||||||
assert isinstance(strategies, list)
|
assert isinstance(strategies, list)
|
||||||
assert len(strategies) == 7
|
assert len(strategies) == 8
|
||||||
# with enum_failed=True search_all_objects() shall find 2 good strategies
|
# with enum_failed=True search_all_objects() shall find 2 good strategies
|
||||||
# and 1 which fails to load
|
# and 1 which fails to load
|
||||||
assert len([x for x in strategies if x['class'] is not None]) == 6
|
assert len([x for x in strategies if x['class'] is not None]) == 7
|
||||||
assert len([x for x in strategies if x['class'] is None]) == 1
|
assert len([x for x in strategies if x['class'] is None]) == 1
|
||||||
|
|
||||||
|
|
||||||
|
@ -2059,8 +2059,9 @@ def test_update_trade_state_orderexception(mocker, default_conf_usdt, caplog) ->
|
|||||||
|
|
||||||
@pytest.mark.parametrize("is_short", [False, True])
|
@pytest.mark.parametrize("is_short", [False, True])
|
||||||
def test_update_trade_state_sell(
|
def test_update_trade_state_sell(
|
||||||
default_conf_usdt, trades_for_order, limit_order_open, limit_order, is_short, mocker,
|
default_conf_usdt, trades_for_order, limit_order_open, limit_order, is_short, mocker
|
||||||
):
|
):
|
||||||
|
buy_order = limit_order[entry_side(is_short)]
|
||||||
open_order = limit_order_open[exit_side(is_short)]
|
open_order = limit_order_open[exit_side(is_short)]
|
||||||
l_order = limit_order[exit_side(is_short)]
|
l_order = limit_order[exit_side(is_short)]
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
|
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
|
||||||
@ -2087,6 +2088,9 @@ def test_update_trade_state_sell(
|
|||||||
leverage=1,
|
leverage=1,
|
||||||
is_short=is_short,
|
is_short=is_short,
|
||||||
)
|
)
|
||||||
|
order = Order.parse_from_ccxt_object(buy_order, 'LTC/ETH', entry_side(is_short))
|
||||||
|
trade.orders.append(order)
|
||||||
|
|
||||||
order = Order.parse_from_ccxt_object(open_order, 'LTC/ETH', exit_side(is_short))
|
order = Order.parse_from_ccxt_object(open_order, 'LTC/ETH', exit_side(is_short))
|
||||||
trade.orders.append(order)
|
trade.orders.append(order)
|
||||||
assert order.status == 'open'
|
assert order.status == 'open'
|
||||||
@ -2790,6 +2794,7 @@ def test_manage_open_orders_partial(
|
|||||||
rpc_mock = patch_RPCManager(mocker)
|
rpc_mock = patch_RPCManager(mocker)
|
||||||
open_trade.is_short = is_short
|
open_trade.is_short = is_short
|
||||||
open_trade.leverage = leverage
|
open_trade.leverage = leverage
|
||||||
|
open_trade.orders[0].ft_order_side = 'sell' if is_short else 'buy'
|
||||||
limit_buy_order_old_partial['id'] = open_trade.open_order_id
|
limit_buy_order_old_partial['id'] = open_trade.open_order_id
|
||||||
limit_buy_order_old_partial['side'] = 'sell' if is_short else 'buy'
|
limit_buy_order_old_partial['side'] = 'sell' if is_short else 'buy'
|
||||||
limit_buy_canceled = deepcopy(limit_buy_order_old_partial)
|
limit_buy_canceled = deepcopy(limit_buy_order_old_partial)
|
||||||
@ -2875,6 +2880,7 @@ def test_manage_open_orders_partial_except(
|
|||||||
limit_buy_order_old_partial_canceled, mocker
|
limit_buy_order_old_partial_canceled, mocker
|
||||||
) -> None:
|
) -> None:
|
||||||
open_trade.is_short = is_short
|
open_trade.is_short = is_short
|
||||||
|
open_trade.orders[0].ft_order_side = 'sell' if is_short else 'buy'
|
||||||
rpc_mock = patch_RPCManager(mocker)
|
rpc_mock = patch_RPCManager(mocker)
|
||||||
limit_buy_order_old_partial_canceled['id'] = open_trade.open_order_id
|
limit_buy_order_old_partial_canceled['id'] = open_trade.open_order_id
|
||||||
limit_buy_order_old_partial['id'] = open_trade.open_order_id
|
limit_buy_order_old_partial['id'] = open_trade.open_order_id
|
||||||
@ -3638,7 +3644,7 @@ def test_execute_trade_exit_market_order(
|
|||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
fetch_ticker=ticker_usdt,
|
fetch_ticker=ticker_usdt,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
_is_dry_limit_order_filled=MagicMock(return_value=False),
|
_is_dry_limit_order_filled=MagicMock(return_value=True),
|
||||||
)
|
)
|
||||||
patch_whitelist(mocker, default_conf_usdt)
|
patch_whitelist(mocker, default_conf_usdt)
|
||||||
freqtrade = FreqtradeBot(default_conf_usdt)
|
freqtrade = FreqtradeBot(default_conf_usdt)
|
||||||
@ -3654,7 +3660,8 @@ def test_execute_trade_exit_market_order(
|
|||||||
# Increase the price and sell it
|
# Increase the price and sell it
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
fetch_ticker=ticker_usdt_sell_up
|
fetch_ticker=ticker_usdt_sell_up,
|
||||||
|
_is_dry_limit_order_filled=MagicMock(return_value=False),
|
||||||
)
|
)
|
||||||
freqtrade.config['order_types']['exit'] = 'market'
|
freqtrade.config['order_types']['exit'] = 'market'
|
||||||
|
|
||||||
@ -3667,7 +3674,7 @@ def test_execute_trade_exit_market_order(
|
|||||||
assert not trade.is_open
|
assert not trade.is_open
|
||||||
assert trade.close_profit == profit_ratio
|
assert trade.close_profit == profit_ratio
|
||||||
|
|
||||||
assert rpc_mock.call_count == 3
|
assert rpc_mock.call_count == 4
|
||||||
last_msg = rpc_mock.call_args_list[-2][0][0]
|
last_msg = rpc_mock.call_args_list[-2][0][0]
|
||||||
assert {
|
assert {
|
||||||
'type': RPCMessageType.EXIT,
|
'type': RPCMessageType.EXIT,
|
||||||
|
@ -72,7 +72,7 @@ def test_add_indicators(default_conf, testdatadir, caplog):
|
|||||||
|
|
||||||
strategy = StrategyResolver.load_strategy(default_conf)
|
strategy = StrategyResolver.load_strategy(default_conf)
|
||||||
|
|
||||||
# Generate buy/sell signals and indicators
|
# Generate entry/exit signals and indicators
|
||||||
data = strategy.analyze_ticker(data, {'pair': pair})
|
data = strategy.analyze_ticker(data, {'pair': pair})
|
||||||
fig = generate_empty_figure()
|
fig = generate_empty_figure()
|
||||||
|
|
||||||
@ -113,7 +113,7 @@ def test_add_areas(default_conf, testdatadir, caplog):
|
|||||||
ind_plain = {"macd": {"fill_to": "macdhist"}}
|
ind_plain = {"macd": {"fill_to": "macdhist"}}
|
||||||
strategy = StrategyResolver.load_strategy(default_conf)
|
strategy = StrategyResolver.load_strategy(default_conf)
|
||||||
|
|
||||||
# Generate buy/sell signals and indicators
|
# Generate entry/exit signals and indicators
|
||||||
data = strategy.analyze_ticker(data, {'pair': pair})
|
data = strategy.analyze_ticker(data, {'pair': pair})
|
||||||
fig = generate_empty_figure()
|
fig = generate_empty_figure()
|
||||||
|
|
||||||
@ -165,24 +165,24 @@ def test_plot_trades(testdatadir, caplog):
|
|||||||
fig = plot_trades(fig, trades)
|
fig = plot_trades(fig, trades)
|
||||||
figure = fig1.layout.figure
|
figure = fig1.layout.figure
|
||||||
|
|
||||||
# Check buys - color, should be in first graph, ...
|
# Check entry - color, should be in first graph, ...
|
||||||
trade_buy = find_trace_in_fig_data(figure.data, 'Trade buy')
|
trade_entries = find_trace_in_fig_data(figure.data, 'Trade entry')
|
||||||
assert isinstance(trade_buy, go.Scatter)
|
assert isinstance(trade_entries, go.Scatter)
|
||||||
assert trade_buy.yaxis == 'y'
|
assert trade_entries.yaxis == 'y'
|
||||||
assert len(trades) == len(trade_buy.x)
|
assert len(trades) == len(trade_entries.x)
|
||||||
assert trade_buy.marker.color == 'cyan'
|
assert trade_entries.marker.color == 'cyan'
|
||||||
assert trade_buy.marker.symbol == 'circle-open'
|
assert trade_entries.marker.symbol == 'circle-open'
|
||||||
assert trade_buy.text[0] == '3.99%, buy_tag, roi, 15 min'
|
assert trade_entries.text[0] == '3.99%, buy_tag, roi, 15 min'
|
||||||
|
|
||||||
trade_sell = find_trace_in_fig_data(figure.data, 'Sell - Profit')
|
trade_exit = find_trace_in_fig_data(figure.data, 'Exit - Profit')
|
||||||
assert isinstance(trade_sell, go.Scatter)
|
assert isinstance(trade_exit, go.Scatter)
|
||||||
assert trade_sell.yaxis == 'y'
|
assert trade_exit.yaxis == 'y'
|
||||||
assert len(trades.loc[trades['profit_ratio'] > 0]) == len(trade_sell.x)
|
assert len(trades.loc[trades['profit_ratio'] > 0]) == len(trade_exit.x)
|
||||||
assert trade_sell.marker.color == 'green'
|
assert trade_exit.marker.color == 'green'
|
||||||
assert trade_sell.marker.symbol == 'square-open'
|
assert trade_exit.marker.symbol == 'square-open'
|
||||||
assert trade_sell.text[0] == '3.99%, buy_tag, roi, 15 min'
|
assert trade_exit.text[0] == '3.99%, buy_tag, roi, 15 min'
|
||||||
|
|
||||||
trade_sell_loss = find_trace_in_fig_data(figure.data, 'Sell - Loss')
|
trade_sell_loss = find_trace_in_fig_data(figure.data, 'Exit - Loss')
|
||||||
assert isinstance(trade_sell_loss, go.Scatter)
|
assert isinstance(trade_sell_loss, go.Scatter)
|
||||||
assert trade_sell_loss.yaxis == 'y'
|
assert trade_sell_loss.yaxis == 'y'
|
||||||
assert len(trades.loc[trades['profit_ratio'] <= 0]) == len(trade_sell_loss.x)
|
assert len(trades.loc[trades['profit_ratio'] <= 0]) == len(trade_sell_loss.x)
|
||||||
|
Loading…
Reference in New Issue
Block a user