Merge branch 'develop' into feat/freqai
This commit is contained in:
commit
f6bfd89cef
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__
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -271,7 +271,8 @@ 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:
|
||||||
|
|
||||||
@ -334,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
|
||||||
@ -348,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)
|
||||||
@ -382,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}']
|
||||||
))
|
))
|
||||||
@ -403,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.
|
||||||
|
|
||||||
@ -867,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,5 +1,6 @@
|
|||||||
|
markdown==3.3.7
|
||||||
mkdocs==1.3.0
|
mkdocs==1.3.0
|
||||||
mkdocs-material==8.3.8
|
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
|
||||||
|
@ -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.
|
||||||
|
@ -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
|
||||||
|
@ -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.
|
||||||
|
@ -314,6 +314,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'},
|
||||||
|
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,7 +77,9 @@ 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
|
||||||
|
"order_props_in_contracts": ['amount', 'cost', 'filled', 'remaining'],
|
||||||
}
|
}
|
||||||
_ft_has: Dict = {}
|
_ft_has: Dict = {}
|
||||||
_ft_has_futures: Dict = {}
|
_ft_has_futures: Dict = {}
|
||||||
@ -174,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(
|
||||||
@ -213,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:
|
||||||
"""
|
"""
|
||||||
@ -422,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
|
||||||
@ -820,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,
|
||||||
@ -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 = float(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 (float(fee['cost']),
|
||||||
order['fee']['currency'],
|
fee['currency'],
|
||||||
self.calculate_fee_rate(order))
|
self.calculate_fee_rate(
|
||||||
|
fee,
|
||||||
|
symbol,
|
||||||
|
cost,
|
||||||
|
amount
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# Historic data
|
# Historic data
|
||||||
|
|
||||||
|
@ -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__)
|
||||||
@ -32,7 +33,9 @@ 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
|
||||||
|
"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]] = [
|
||||||
@ -95,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(
|
||||||
|
@ -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]] = [
|
||||||
|
@ -332,6 +332,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}"
|
||||||
@ -634,7 +636,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,
|
||||||
@ -814,7 +816,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(
|
||||||
@ -1465,7 +1467,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:
|
||||||
@ -1742,7 +1744,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:
|
||||||
@ -1780,7 +1783,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_)
|
||||||
|
@ -727,7 +727,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,
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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:
|
||||||
@ -306,6 +323,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"*{'Profit' if is_fill else 'Unrealized Profit'}:* "
|
f"*{'Profit' if is_fill else 'Unrealized Profit'}:* "
|
||||||
f"`{msg['profit_ratio']:.2%}{msg['profit_extra']}`\n"
|
f"`{msg['profit_ratio']:.2%}{msg['profit_extra']}`\n"
|
||||||
f"*Enter Tag:* `{msg['enter_tag']}`\n"
|
f"*Enter Tag:* `{msg['enter_tag']}`\n"
|
||||||
|
@ -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.
|
||||||
|
@ -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",
|
||||||
|
@ -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.
|
||||||
|
@ -8,11 +8,11 @@ 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.8.1
|
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
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
numpy==1.23.0
|
numpy==1.23.1
|
||||||
pandas==1.4.3
|
pandas==1.4.3
|
||||||
pandas-ta==0.3.14b
|
pandas-ta==0.3.14b
|
||||||
|
|
||||||
ccxt==1.89.14
|
ccxt==1.90.47
|
||||||
# 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.39
|
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.10
|
tabulate==0.8.10
|
||||||
@ -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.3
|
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.18.1
|
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
|
||||||
|
|
||||||
|
@ -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))
|
||||||
@ -1694,6 +1691,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 +3163,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": [
|
||||||
{
|
{
|
||||||
|
@ -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:
|
||||||
|
@ -1135,7 +1135,7 @@ 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,startprice,endprice", [
|
@pytest.mark.parametrize("side,startprice,endprice", [
|
||||||
@ -3544,7 +3544,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 +3582,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 +3593,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', [
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
@ -1655,8 +1656,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,
|
||||||
@ -1674,6 +1684,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)
|
||||||
@ -1683,6 +1694,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}'
|
||||||
@ -1710,7 +1722,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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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