Merge branch 'develop' into relative_stake
This commit is contained in:
commit
a75420f75f
@ -78,13 +78,17 @@ Please also read about the [strategy startup period](strategy-customization.md#s
|
|||||||
#### Supplying custom fee value
|
#### Supplying custom fee value
|
||||||
|
|
||||||
Sometimes your account has certain fee rebates (fee reductions starting with a certain account size or monthly volume), which are not visible to ccxt.
|
Sometimes your account has certain fee rebates (fee reductions starting with a certain account size or monthly volume), which are not visible to ccxt.
|
||||||
To account for this in backtesting, you can use `--fee 0.001` to supply this value to backtesting.
|
To account for this in backtesting, you can use the `--fee` command line option to supply this value to backtesting.
|
||||||
This fee must be a percentage, and will be applied twice (once for trade entry, and once for trade exit).
|
This fee must be a ratio, and will be applied twice (once for trade entry, and once for trade exit).
|
||||||
|
|
||||||
|
For example, if the buying and selling commission fee is 0.1% (i.e., 0.001 written as ratio), then you would run backtesting as the following:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
freqtrade backtesting --fee 0.001
|
freqtrade backtesting --fee 0.001
|
||||||
```
|
```
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
Only supply this option (or the corresponding configuration parameter) if you want to experiment with different fee values. By default, Backtesting fetches the default fee from the exchange pair/market info.
|
||||||
|
|
||||||
#### Running backtest with smaller testset by using timerange
|
#### Running backtest with smaller testset by using timerange
|
||||||
|
|
||||||
|
@ -270,3 +270,18 @@ The easiest way is to download install Microsoft Visual Studio Community [here](
|
|||||||
|
|
||||||
Now you have an environment ready, the next step is
|
Now you have an environment ready, the next step is
|
||||||
[Bot Configuration](configuration.md).
|
[Bot Configuration](configuration.md).
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### MacOS installation error
|
||||||
|
|
||||||
|
Newer versions of MacOS may have installation failed with errors like `error: command 'g++' failed with exit status 1`.
|
||||||
|
|
||||||
|
This error will require explicit installation of the SDK Headers, which are not installed by default in this version of MacOS.
|
||||||
|
For MacOS 10.14, this can be accomplished with the below command.
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
open /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10.14.pkg
|
||||||
|
```
|
||||||
|
|
||||||
|
If this file is inexistant, then you're probably on a different version of MacOS, so you may need to consult the internet for specific resolution details.
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
|
from copy import deepcopy
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
from jsonschema import Draft4Validator, validators
|
from jsonschema import Draft4Validator, validators
|
||||||
@ -42,15 +43,25 @@ def validate_config_schema(conf: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
:param conf: Config in JSON format
|
:param conf: Config in JSON format
|
||||||
:return: Returns the config if valid, otherwise throw an exception
|
:return: Returns the config if valid, otherwise throw an exception
|
||||||
"""
|
"""
|
||||||
|
conf_schema = deepcopy(constants.CONF_SCHEMA)
|
||||||
|
if conf.get('runmode', RunMode.OTHER) in (RunMode.DRY_RUN, RunMode.LIVE):
|
||||||
|
conf_schema['required'] = constants.SCHEMA_TRADE_REQUIRED
|
||||||
|
else:
|
||||||
|
conf_schema['required'] = constants.SCHEMA_MINIMAL_REQUIRED
|
||||||
|
# Dynamically allow empty stake-currency
|
||||||
|
# Since the minimal config specifies this too.
|
||||||
|
# It's not allowed for Dry-run or live modes
|
||||||
|
conf_schema['properties']['stake_currency']['enum'] += [''] # type: ignore
|
||||||
|
|
||||||
try:
|
try:
|
||||||
FreqtradeValidator(constants.CONF_SCHEMA).validate(conf)
|
FreqtradeValidator(conf_schema).validate(conf)
|
||||||
return conf
|
return conf
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
logger.critical(
|
logger.critical(
|
||||||
f"Invalid configuration. See config.json.example. Reason: {e}"
|
f"Invalid configuration. See config.json.example. Reason: {e}"
|
||||||
)
|
)
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
best_match(Draft4Validator(constants.CONF_SCHEMA).iter_errors(conf)).message
|
best_match(Draft4Validator(conf_schema).iter_errors(conf)).message
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -275,7 +275,9 @@ CONF_SCHEMA = {
|
|||||||
'required': ['process_throttle_secs', 'allowed_risk']
|
'required': ['process_throttle_secs', 'allowed_risk']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'required': [
|
}
|
||||||
|
|
||||||
|
SCHEMA_TRADE_REQUIRED = [
|
||||||
'exchange',
|
'exchange',
|
||||||
'max_open_trades',
|
'max_open_trades',
|
||||||
'stake_currency',
|
'stake_currency',
|
||||||
@ -287,4 +289,8 @@ CONF_SCHEMA = {
|
|||||||
'stoploss',
|
'stoploss',
|
||||||
'minimal_roi',
|
'minimal_roi',
|
||||||
]
|
]
|
||||||
}
|
|
||||||
|
SCHEMA_MINIMAL_REQUIRED = [
|
||||||
|
'exchange',
|
||||||
|
'dry_run',
|
||||||
|
]
|
||||||
|
@ -47,7 +47,7 @@ def load_backtest_data(filename) -> pd.DataFrame:
|
|||||||
utc=True,
|
utc=True,
|
||||||
infer_datetime_format=True
|
infer_datetime_format=True
|
||||||
)
|
)
|
||||||
df['profitabs'] = df['close_rate'] - df['open_rate']
|
df['profit'] = df['close_rate'] - df['open_rate']
|
||||||
df = df.sort_values("open_time").reset_index(drop=True)
|
df = df.sort_values("open_time").reset_index(drop=True)
|
||||||
return df
|
return df
|
||||||
|
|
||||||
|
@ -419,8 +419,6 @@ class FreqtradeBot:
|
|||||||
:param pair: pair for which we want to create a LIMIT_BUY
|
:param pair: pair for which we want to create a LIMIT_BUY
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
stake_currency = self.config['stake_currency']
|
|
||||||
fiat_currency = self.config.get('fiat_display_currency', None)
|
|
||||||
time_in_force = self.strategy.order_time_in_force['buy']
|
time_in_force = self.strategy.order_time_in_force['buy']
|
||||||
|
|
||||||
if price:
|
if price:
|
||||||
@ -477,17 +475,6 @@ class FreqtradeBot:
|
|||||||
amount = order['amount']
|
amount = order['amount']
|
||||||
buy_limit_filled_price = order['price']
|
buy_limit_filled_price = order['price']
|
||||||
|
|
||||||
self.rpc.send_msg({
|
|
||||||
'type': RPCMessageType.BUY_NOTIFICATION,
|
|
||||||
'exchange': self.exchange.name.capitalize(),
|
|
||||||
'pair': pair,
|
|
||||||
'limit': buy_limit_filled_price,
|
|
||||||
'order_type': order_type,
|
|
||||||
'stake_amount': stake_amount,
|
|
||||||
'stake_currency': stake_currency,
|
|
||||||
'fiat_currency': fiat_currency
|
|
||||||
})
|
|
||||||
|
|
||||||
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
|
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
|
||||||
fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker')
|
fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker')
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
@ -505,6 +492,8 @@ class FreqtradeBot:
|
|||||||
ticker_interval=timeframe_to_minutes(self.config['ticker_interval'])
|
ticker_interval=timeframe_to_minutes(self.config['ticker_interval'])
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self._notify_buy(trade, order_type)
|
||||||
|
|
||||||
# Update fees if order is closed
|
# Update fees if order is closed
|
||||||
if order_status == 'closed':
|
if order_status == 'closed':
|
||||||
self.update_trade_state(trade, order)
|
self.update_trade_state(trade, order)
|
||||||
@ -517,6 +506,24 @@ class FreqtradeBot:
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def _notify_buy(self, trade: Trade, order_type: str):
|
||||||
|
"""
|
||||||
|
Sends rpc notification when a buy occured.
|
||||||
|
"""
|
||||||
|
msg = {
|
||||||
|
'type': RPCMessageType.BUY_NOTIFICATION,
|
||||||
|
'exchange': self.exchange.name.capitalize(),
|
||||||
|
'pair': trade.pair,
|
||||||
|
'limit': trade.open_rate,
|
||||||
|
'order_type': order_type,
|
||||||
|
'stake_amount': trade.stake_amount,
|
||||||
|
'stake_currency': self.config['stake_currency'],
|
||||||
|
'fiat_currency': self.config.get('fiat_display_currency', None),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Send the message
|
||||||
|
self.rpc.send_msg(msg)
|
||||||
|
|
||||||
#
|
#
|
||||||
# SELL / exit positions / close trades logic and methods
|
# SELL / exit positions / close trades logic and methods
|
||||||
#
|
#
|
||||||
@ -919,16 +926,16 @@ class FreqtradeBot:
|
|||||||
except InvalidOrderException:
|
except InvalidOrderException:
|
||||||
logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}")
|
logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}")
|
||||||
|
|
||||||
ordertype = self.strategy.order_types[sell_type]
|
order_type = self.strategy.order_types[sell_type]
|
||||||
if sell_reason == SellType.EMERGENCY_SELL:
|
if sell_reason == SellType.EMERGENCY_SELL:
|
||||||
# Emergencysells (default to market!)
|
# Emergencysells (default to market!)
|
||||||
ordertype = self.strategy.order_types.get("emergencysell", "market")
|
order_type = self.strategy.order_types.get("emergencysell", "market")
|
||||||
|
|
||||||
amount = self._safe_sell_amount(trade.pair, trade.amount)
|
amount = self._safe_sell_amount(trade.pair, trade.amount)
|
||||||
|
|
||||||
# Execute sell and update trade record
|
# Execute sell and update trade record
|
||||||
order = self.exchange.sell(pair=str(trade.pair),
|
order = self.exchange.sell(pair=str(trade.pair),
|
||||||
ordertype=ordertype,
|
ordertype=order_type,
|
||||||
amount=amount, rate=limit,
|
amount=amount, rate=limit,
|
||||||
time_in_force=self.strategy.order_time_in_force['sell']
|
time_in_force=self.strategy.order_time_in_force['sell']
|
||||||
)
|
)
|
||||||
@ -944,7 +951,7 @@ class FreqtradeBot:
|
|||||||
# Lock pair for one candle to prevent immediate rebuys
|
# Lock pair for one candle to prevent immediate rebuys
|
||||||
self.strategy.lock_pair(trade.pair, timeframe_to_next_date(self.config['ticker_interval']))
|
self.strategy.lock_pair(trade.pair, timeframe_to_next_date(self.config['ticker_interval']))
|
||||||
|
|
||||||
self._notify_sell(trade, ordertype)
|
self._notify_sell(trade, order_type)
|
||||||
|
|
||||||
def _notify_sell(self, trade: Trade, order_type: str):
|
def _notify_sell(self, trade: Trade, order_type: str):
|
||||||
"""
|
"""
|
||||||
@ -971,16 +978,13 @@ class FreqtradeBot:
|
|||||||
'profit_percent': profit_percent,
|
'profit_percent': profit_percent,
|
||||||
'sell_reason': trade.sell_reason,
|
'sell_reason': trade.sell_reason,
|
||||||
'open_date': trade.open_date,
|
'open_date': trade.open_date,
|
||||||
'close_date': trade.close_date or datetime.utcnow()
|
'close_date': trade.close_date or datetime.utcnow(),
|
||||||
|
'stake_currency': self.config['stake_currency'],
|
||||||
}
|
}
|
||||||
|
|
||||||
# For regular case, when the configuration exists
|
if 'fiat_display_currency' in self.config:
|
||||||
if 'stake_currency' in self.config and 'fiat_display_currency' in self.config:
|
|
||||||
stake_currency = self.config['stake_currency']
|
|
||||||
fiat_currency = self.config['fiat_display_currency']
|
|
||||||
msg.update({
|
msg.update({
|
||||||
'stake_currency': stake_currency,
|
'fiat_currency': self.config['fiat_display_currency'],
|
||||||
'fiat_currency': fiat_currency,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
# Send the message
|
# Send the message
|
||||||
|
@ -120,8 +120,8 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
# Create description for sell summarizing the trade
|
# Create description for sell summarizing the trade
|
||||||
desc = trades.apply(lambda row: f"{round(row['profitperc'], 3)}%, {row['sell_reason']}, "
|
desc = trades.apply(lambda row: f"{round(row['profitperc'] * 100, 1)}%, "
|
||||||
f"{row['duration']} min",
|
f"{row['sell_reason']}, {row['duration']} min",
|
||||||
axis=1)
|
axis=1)
|
||||||
trade_sells = go.Scatter(
|
trade_sells = go.Scatter(
|
||||||
x=trades["close_time"],
|
x=trades["close_time"],
|
||||||
|
@ -12,7 +12,8 @@ from colorama import init as colorama_init
|
|||||||
from tabulate import tabulate
|
from tabulate import tabulate
|
||||||
|
|
||||||
from freqtrade.configuration import (Configuration, TimeRange,
|
from freqtrade.configuration import (Configuration, TimeRange,
|
||||||
remove_credentials)
|
remove_credentials,
|
||||||
|
validate_config_consistency)
|
||||||
from freqtrade.configuration.directory_operations import (copy_sample_files,
|
from freqtrade.configuration.directory_operations import (copy_sample_files,
|
||||||
create_userdata_dir)
|
create_userdata_dir)
|
||||||
from freqtrade.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGY
|
from freqtrade.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGY
|
||||||
@ -40,6 +41,7 @@ def setup_utils_configuration(args: Dict[str, Any], method: RunMode) -> Dict[str
|
|||||||
|
|
||||||
# Ensure we do not use Exchange credentials
|
# Ensure we do not use Exchange credentials
|
||||||
remove_credentials(config)
|
remove_credentials(config)
|
||||||
|
validate_config_consistency(config)
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ def test_load_backtest_data(testdatadir):
|
|||||||
filename = testdatadir / "backtest-result_test.json"
|
filename = testdatadir / "backtest-result_test.json"
|
||||||
bt_data = load_backtest_data(filename)
|
bt_data = load_backtest_data(filename)
|
||||||
assert isinstance(bt_data, DataFrame)
|
assert isinstance(bt_data, DataFrame)
|
||||||
assert list(bt_data.columns) == BT_DATA_COLUMNS + ["profitabs"]
|
assert list(bt_data.columns) == BT_DATA_COLUMNS + ["profit"]
|
||||||
assert len(bt_data) == 179
|
assert len(bt_data) == 179
|
||||||
|
|
||||||
# Test loading from string (must yield same result)
|
# Test loading from string (must yield same result)
|
||||||
|
@ -49,6 +49,7 @@ def test_load_config_missing_attributes(default_conf) -> None:
|
|||||||
|
|
||||||
conf = deepcopy(default_conf)
|
conf = deepcopy(default_conf)
|
||||||
conf.pop('stake_currency')
|
conf.pop('stake_currency')
|
||||||
|
conf['runmode'] = RunMode.DRY_RUN
|
||||||
with pytest.raises(ValidationError, match=r".*'stake_currency' is a required property.*"):
|
with pytest.raises(ValidationError, match=r".*'stake_currency' is a required property.*"):
|
||||||
validate_config_schema(conf)
|
validate_config_schema(conf)
|
||||||
|
|
||||||
|
@ -119,6 +119,7 @@ def test_plot_trades(testdatadir, caplog):
|
|||||||
assert trade_sell.yaxis == 'y'
|
assert trade_sell.yaxis == 'y'
|
||||||
assert len(trades) == len(trade_sell.x)
|
assert len(trades) == len(trade_sell.x)
|
||||||
assert trade_sell.marker.color == 'red'
|
assert trade_sell.marker.color == 'red'
|
||||||
|
assert trade_sell.text[0] == "4.0%, roi, 15 min"
|
||||||
|
|
||||||
|
|
||||||
def test_generate_candlestick_graph_no_signals_no_trades(default_conf, mocker, testdatadir, caplog):
|
def test_generate_candlestick_graph_no_signals_no_trades(default_conf, mocker, testdatadir, caplog):
|
||||||
|
Loading…
Reference in New Issue
Block a user