Merge pull request #2661 from freqtrade/wallet_dry

Introduce Dry-Run Wallet
This commit is contained in:
hroff-1902 2019-12-16 14:00:11 +03:00 committed by GitHub
commit 39197458f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 111 additions and 31 deletions

View File

@ -47,7 +47,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `ticker_interval` | The ticker interval to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). [Strategy Override](#parameters-in-the-strategy). <br> ***Datatype:*** *String* | `ticker_interval` | The ticker interval to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). [Strategy Override](#parameters-in-the-strategy). <br> ***Datatype:*** *String*
| `fiat_display_currency` | Fiat currency used to show your profits. [More information below](#what-values-can-be-used-for-fiat_display_currency). <br> ***Datatype:*** *String* | `fiat_display_currency` | Fiat currency used to show your profits. [More information below](#what-values-can-be-used-for-fiat_display_currency). <br> ***Datatype:*** *String*
| `dry_run` | **Required.** Define if the bot must be in Dry Run or production mode. <br>*Defaults to `true`.* <br> ***Datatype:*** *Boolean* | `dry_run` | **Required.** Define if the bot must be in Dry Run or production mode. <br>*Defaults to `true`.* <br> ***Datatype:*** *Boolean*
| `dry_run_wallet` | Overrides the default amount of 999.9 stake currency units in the wallet used by the bot running in the Dry Run mode if you need it for any reason. <br> ***Datatype:*** *Float* | `dry_run_wallet` | Define the starting amount in stake currency for the simulated wallet used by the bot running in the Dry Run mode.<br>*Defaults to `1000`.* <br> ***Datatype:*** *Float*
| `process_only_new_candles` | Enable processing of indicators only when new candles arrive. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> ***Datatype:*** *Boolean* | `process_only_new_candles` | Enable processing of indicators only when new candles arrive. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> ***Datatype:*** *Boolean*
| `minimal_roi` | **Required.** Set the threshold in percent the bot will use to sell a trade. [More information below](#understand-minimal_roi). [Strategy Override](#parameters-in-the-strategy). <br> ***Datatype:*** *Dict* | `minimal_roi` | **Required.** Set the threshold in percent the bot will use to sell a trade. [More information below](#understand-minimal_roi). [Strategy Override](#parameters-in-the-strategy). <br> ***Datatype:*** *Dict*
| `stoploss` | **Required.** Value of the stoploss in percent used by the bot. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br> ***Datatype:*** *Float (as ratio)* | `stoploss` | **Required.** Value of the stoploss in percent used by the bot. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br> ***Datatype:*** *Float (as ratio)*
@ -149,6 +149,9 @@ In this case a trade amount is calculated as:
currency_balance / (max_open_trades - current_open_trades) currency_balance / (max_open_trades - current_open_trades)
``` ```
!!! Note "When using Dry-Run Mode"
When using `"stake_amount" : "unlimited",` in combination with Dry-Run, the balance will be simulated starting with a stake of `dry_run_wallet` which will evolve over time. It is therefore important to set `dry_run_wallet` to a sensible value (like 0.05 or 0.01 for BTC and 1000 or 100 for USDT, for example), otherwise it may simulate trades with 100 BTC (or more) or 0.05 USDT (or less) at once - which may not correspond to your real available balance or is less than the exchange minimal limit for the order amount for the stake currency.
### Understand minimal_roi ### Understand minimal_roi
The `minimal_roi` configuration parameter is a JSON object where the key is a duration The `minimal_roi` configuration parameter is a JSON object where the key is a duration
@ -501,8 +504,10 @@ creating trades on the exchange.
} }
``` ```
Once you will be happy with your bot performance running in the Dry-run mode, Once you will be happy with your bot performance running in the Dry-run mode, you can switch it to production mode.
you can switch it to production mode.
!!! Note
A simulated wallet is available during dry-run mode, and will assume a starting capital of `dry_run_wallet` (defaults to 1000).
## Switch to production mode ## Switch to production mode
@ -532,7 +537,7 @@ you run it in production mode.
``` ```
!!! Note !!! Note
If you have an exchange API key yet, [see our tutorial](/pre-requisite). If you have an exchange API key yet, [see our tutorial](installation.md#setup-your-exchange-account).
You should also make sure to read the [Exchanges](exchanges.md) section of the documentation to be aware of potential configuration details specific to your exchange. You should also make sure to read the [Exchanges](exchanges.md) section of the documentation to be aware of potential configuration details specific to your exchange.

View File

@ -18,7 +18,7 @@ REQUIRED_ORDERTYPES = ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']
ORDERTYPE_POSSIBILITIES = ['limit', 'market'] ORDERTYPE_POSSIBILITIES = ['limit', 'market']
ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc'] ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc']
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'PrecisionFilter', 'PriceFilter'] AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'PrecisionFilter', 'PriceFilter']
DRY_RUN_WALLET = 999.9 DRY_RUN_WALLET = 1000
MATH_CLOSE_PREC = 1e-14 # Precision used for float comparisons MATH_CLOSE_PREC = 1e-14 # Precision used for float comparisons
USERPATH_HYPEROPTS = 'hyperopts' USERPATH_HYPEROPTS = 'hyperopts'
@ -75,7 +75,7 @@ CONF_SCHEMA = {
}, },
'fiat_display_currency': {'type': 'string', 'enum': SUPPORTED_FIAT}, 'fiat_display_currency': {'type': 'string', 'enum': SUPPORTED_FIAT},
'dry_run': {'type': 'boolean'}, 'dry_run': {'type': 'boolean'},
'dry_run_wallet': {'type': 'number'}, 'dry_run_wallet': {'type': 'number', 'default': DRY_RUN_WALLET},
'process_only_new_candles': {'type': 'boolean'}, 'process_only_new_candles': {'type': 'boolean'},
'minimal_roi': { 'minimal_roi': {
'type': 'object', 'type': 'object',
@ -275,6 +275,7 @@ CONF_SCHEMA = {
'stake_currency', 'stake_currency',
'stake_amount', 'stake_amount',
'dry_run', 'dry_run',
'dry_run_wallet',
'bid_strategy', 'bid_strategy',
'unfilledtimeout', 'unfilledtimeout',
'stoploss', 'stoploss',

View File

@ -18,7 +18,7 @@ from ccxt.base.decimal_to_precision import ROUND_DOWN, ROUND_UP
from pandas import DataFrame from pandas import DataFrame
from freqtrade import (DependencyException, InvalidOrderException, from freqtrade import (DependencyException, InvalidOrderException,
OperationalException, TemporaryError, constants) OperationalException, TemporaryError)
from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.converter import parse_ticker_dataframe
from freqtrade.exchange.common import BAD_EXCHANGES, retrier, retrier_async from freqtrade.exchange.common import BAD_EXCHANGES, retrier, retrier_async
from freqtrade.misc import deep_merge_dicts from freqtrade.misc import deep_merge_dicts
@ -479,7 +479,7 @@ class Exchange:
@retrier @retrier
def get_balance(self, currency: str) -> float: def get_balance(self, currency: str) -> float:
if self._config['dry_run']: if self._config['dry_run']:
return constants.DRY_RUN_WALLET return self._config['dry_run_wallet']
# ccxt exception is already handled by get_balances # ccxt exception is already handled by get_balances
balances = self.get_balances() balances = self.get_balances()

View File

@ -62,7 +62,11 @@ class FreqtradeBot:
self.exchange = ExchangeResolver(self.config['exchange']['name'], self.config).exchange self.exchange = ExchangeResolver(self.config['exchange']['name'], self.config).exchange
persistence.init(self.config.get('db_url', None),
clean_open_orders=self.config.get('dry_run', False))
self.wallets = Wallets(self.config, self.exchange) self.wallets = Wallets(self.config, self.exchange)
self.dataprovider = DataProvider(self.config, self.exchange) self.dataprovider = DataProvider(self.config, self.exchange)
# Attach Dataprovider to Strategy baseclass # Attach Dataprovider to Strategy baseclass
@ -78,9 +82,6 @@ class FreqtradeBot:
self.active_pair_whitelist = self._refresh_whitelist() self.active_pair_whitelist = self._refresh_whitelist()
persistence.init(self.config.get('db_url', None),
clean_open_orders=self.config.get('dry_run', False))
# Set initial bot state from config # Set initial bot state from config
initial_state = self.config.get('initial_state') initial_state = self.config.get('initial_state')
self.state = State[initial_state.upper()] if initial_state else State.STOPPED self.state = State[initial_state.upper()] if initial_state else State.STOPPED
@ -231,8 +232,8 @@ class FreqtradeBot:
# Check if stake_amount is fulfilled # Check if stake_amount is fulfilled
if available_amount < stake_amount: if available_amount < stake_amount:
raise DependencyException( raise DependencyException(
f"Available balance({available_amount} {self.config['stake_currency']}) is " f"Available balance ({available_amount} {self.config['stake_currency']}) is "
f"lower than stake amount({stake_amount} {self.config['stake_currency']})" f"lower than stake amount ({stake_amount} {self.config['stake_currency']})"
) )
return stake_amount return stake_amount

View File

@ -348,6 +348,7 @@ class RPC:
'total': total, 'total': total,
'symbol': symbol, 'symbol': symbol,
'value': value, 'value': value,
'note': 'Simulated balances' if self._freqtrade.config.get('dry_run', False) else ''
} }
def _rpc_start(self) -> Dict[str, str]: def _rpc_start(self) -> Dict[str, str]:

View File

@ -331,7 +331,15 @@ class Telegram(RPC):
try: try:
result = self._rpc_balance(self._config['stake_currency'], result = self._rpc_balance(self._config['stake_currency'],
self._config.get('fiat_display_currency', '')) self._config.get('fiat_display_currency', ''))
output = '' output = ''
if self._config['dry_run']:
output += (
f"*Warning:*Simulated balances in Dry Mode.\n"
"This mode is still experimental!\n"
"Starting capital: "
f"`{self._config['dry_run_wallet']}` {self._config['stake_currency']}.\n"
)
for currency in result['currencies']: for currency in result['currencies']:
if currency['est_stake'] > 0.0001: if currency['est_stake'] > 0.0001:
curr_output = "*{currency}:*\n" \ curr_output = "*{currency}:*\n" \

View File

@ -4,7 +4,7 @@
import logging import logging
from typing import Dict, NamedTuple, Any from typing import Dict, NamedTuple, Any
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade import constants from freqtrade.persistence import Trade
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -23,14 +23,12 @@ class Wallets:
self._config = config self._config = config
self._exchange = exchange self._exchange = exchange
self._wallets: Dict[str, Wallet] = {} self._wallets: Dict[str, Wallet] = {}
self.start_cap = config['dry_run_wallet']
self.update() self.update()
def get_free(self, currency) -> float: def get_free(self, currency) -> float:
if self._config['dry_run']:
return self._config.get('dry_run_wallet', constants.DRY_RUN_WALLET)
balance = self._wallets.get(currency) balance = self._wallets.get(currency)
if balance and balance.free: if balance and balance.free:
return balance.free return balance.free
@ -39,9 +37,6 @@ class Wallets:
def get_used(self, currency) -> float: def get_used(self, currency) -> float:
if self._config['dry_run']:
return self._config.get('dry_run_wallet', constants.DRY_RUN_WALLET)
balance = self._wallets.get(currency) balance = self._wallets.get(currency)
if balance and balance.used: if balance and balance.used:
return balance.used return balance.used
@ -50,16 +45,42 @@ class Wallets:
def get_total(self, currency) -> float: def get_total(self, currency) -> float:
if self._config['dry_run']:
return self._config.get('dry_run_wallet', constants.DRY_RUN_WALLET)
balance = self._wallets.get(currency) balance = self._wallets.get(currency)
if balance and balance.total: if balance and balance.total:
return balance.total return balance.total
else: else:
return 0 return 0
def update(self) -> None: def _update_dry(self) -> None:
"""
Update from database in dry-run mode
- Apply apply profits of closed trades on top of stake amount
- Subtract currently tied up stake_amount in open trades
- update balances for currencies currently in trades
"""
closed_trades = Trade.get_trades(Trade.is_open.is_(False)).all()
open_trades = Trade.get_trades(Trade.is_open.is_(True)).all()
tot_profit = sum([trade.calc_profit() for trade in closed_trades])
tot_in_trades = sum([trade.stake_amount for trade in open_trades])
current_stake = self.start_cap + tot_profit - tot_in_trades
self._wallets[self._config['stake_currency']] = Wallet(
self._config['stake_currency'],
current_stake,
0,
current_stake
)
for trade in open_trades:
curr = trade.pair.split('/')[0]
self._wallets[curr] = Wallet(
curr,
trade.amount,
0,
trade.amount
)
def _update_live(self) -> None:
balances = self._exchange.get_balances() balances = self._exchange.get_balances()
@ -71,6 +92,11 @@ class Wallets:
balances[currency].get('total', None) balances[currency].get('total', None)
) )
def update(self) -> None:
if self._config['dry_run']:
self._update_dry()
else:
self._update_live()
logger.info('Wallets synced.') logger.info('Wallets synced.')
def get_all_balances(self) -> Dict[str, Any]: def get_all_balances(self) -> Dict[str, Any]:

View File

@ -876,6 +876,7 @@ def test_sell_considers_time_in_force(default_conf, mocker, exchange_name):
def test_get_balance_dry_run(default_conf, mocker): def test_get_balance_dry_run(default_conf, mocker):
default_conf['dry_run'] = True default_conf['dry_run'] = True
default_conf['dry_run_wallet'] = 999.9
exchange = get_patched_exchange(mocker, default_conf) exchange = get_patched_exchange(mocker, default_conf)
assert exchange.get_balance(currency='BTC') == 999.9 assert exchange.get_balance(currency='BTC') == 999.9

View File

@ -398,7 +398,7 @@ def test_rpc_balance_handle(default_conf, mocker, tickers):
get_valid_pair_combination=MagicMock( get_valid_pair_combination=MagicMock(
side_effect=lambda a, b: f"{b}/{a}" if a == "USDT" else f"{a}/{b}") side_effect=lambda a, b: f"{b}/{a}" if a == "USDT" else f"{a}/{b}")
) )
default_conf['dry_run'] = False
freqtradebot = get_patched_freqtradebot(mocker, default_conf) freqtradebot = get_patched_freqtradebot(mocker, default_conf)
patch_get_signal(freqtradebot, (True, False)) patch_get_signal(freqtradebot, (True, False))
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)

View File

@ -230,6 +230,7 @@ def test_api_stopbuy(botclient):
def test_api_balance(botclient, mocker, rpc_balance): def test_api_balance(botclient, mocker, rpc_balance):
ftbot, client = botclient ftbot, client = botclient
ftbot.config['dry_run'] = False
mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance) mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance)
mocker.patch('freqtrade.exchange.Exchange.get_valid_pair_combination', mocker.patch('freqtrade.exchange.Exchange.get_valid_pair_combination',
side_effect=lambda a, b: f"{a}/{b}") side_effect=lambda a, b: f"{a}/{b}")

View File

@ -462,7 +462,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tickers) -> None: def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tickers) -> None:
default_conf['dry_run'] = False
mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance) mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance)
mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers) mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers)
mocker.patch('freqtrade.exchange.Exchange.get_valid_pair_combination', mocker.patch('freqtrade.exchange.Exchange.get_valid_pair_combination',
@ -494,6 +494,7 @@ def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tick
def test_balance_handle_empty_response(default_conf, update, mocker) -> None: def test_balance_handle_empty_response(default_conf, update, mocker) -> None:
default_conf['dry_run'] = False
mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value={}) mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value={})
msg_mock = MagicMock() msg_mock = MagicMock()
@ -533,7 +534,8 @@ def test_balance_handle_empty_response_dry(default_conf, update, mocker) -> None
telegram._balance(update=update, context=MagicMock()) telegram._balance(update=update, context=MagicMock())
result = msg_mock.call_args_list[0][0][0] result = msg_mock.call_args_list[0][0][0]
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert "Running in Dry Run, balances are not available." in result assert "*Warning:*Simulated balances in Dry Mode." in result
assert "Starting capital: `1000` BTC" in result
def test_balance_handle_too_large_response(default_conf, update, mocker) -> None: def test_balance_handle_too_large_response(default_conf, update, mocker) -> None:

View File

@ -8,9 +8,9 @@ from pathlib import Path
from unittest.mock import MagicMock from unittest.mock import MagicMock
import pytest import pytest
from jsonschema import Draft4Validator, ValidationError, validate from jsonschema import ValidationError
from freqtrade import OperationalException, constants from freqtrade import OperationalException
from freqtrade.configuration import (Arguments, Configuration, check_exchange, from freqtrade.configuration import (Arguments, Configuration, check_exchange,
remove_credentials, remove_credentials,
validate_config_consistency) validate_config_consistency)
@ -718,7 +718,8 @@ def test_load_config_warn_forcebuy(default_conf, mocker, caplog) -> None:
def test_validate_default_conf(default_conf) -> None: def test_validate_default_conf(default_conf) -> None:
validate(default_conf, constants.CONF_SCHEMA, Draft4Validator) # Validate via our validator - we allow setting defaults!
validate_config_schema(default_conf)
def test_validate_tsl(default_conf): def test_validate_tsl(default_conf):

View File

@ -211,6 +211,7 @@ def test_edge_overrides_stake_amount(mocker, edge_conf) -> None:
patch_RPCManager(mocker) patch_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)
patch_edge(mocker) patch_edge(mocker)
edge_conf['dry_run_wallet'] = 999.9
freqtrade = FreqtradeBot(edge_conf) freqtrade = FreqtradeBot(edge_conf)
assert freqtrade._get_trade_stake_amount('NEO/BTC') == (999.9 * 0.5 * 0.01) / 0.20 assert freqtrade._get_trade_stake_amount('NEO/BTC') == (999.9 * 0.5 * 0.01) / 0.20
@ -1338,6 +1339,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog,
patch_exchange(mocker) patch_exchange(mocker)
patch_edge(mocker) patch_edge(mocker)
edge_conf['max_open_trades'] = float('inf') edge_conf['max_open_trades'] = float('inf')
edge_conf['dry_run_wallet'] = 999.9
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
get_ticker=MagicMock(return_value={ get_ticker=MagicMock(return_value={
@ -3483,3 +3485,32 @@ def test_process_i_am_alive(default_conf, mocker, caplog):
ftbot.process() ftbot.process()
assert log_has_re(message, caplog) assert log_has_re(message, caplog)
@pytest.mark.usefixtures("init_persistence")
def test_sync_wallet_dry_run(mocker, default_conf, ticker, fee, limit_buy_order):
default_conf['dry_run'] = True
# Initialize to 2 times stake amount
default_conf['dry_run_wallet'] = 0.002
default_conf['max_open_trades'] = 2
patch_exchange(mocker)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
get_ticker=ticker,
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
get_fee=fee,
)
bot = get_patched_freqtradebot(mocker, default_conf)
patch_get_signal(bot)
assert bot.wallets.get_free('BTC') == 0.002
bot.create_trades()
trades = Trade.query.all()
assert len(trades) == 2
bot.config['max_open_trades'] = 3
with pytest.raises(
DependencyException,
match=r"Available balance \(0 BTC\) is lower than stake amount \(0.001 BTC\)"):
bot.create_trades()

View File

@ -71,6 +71,7 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee,
) )
mocker.patch("freqtrade.strategy.interface.IStrategy.should_sell", should_sell_mock) mocker.patch("freqtrade.strategy.interface.IStrategy.should_sell", should_sell_mock)
wallets_mock = mocker.patch("freqtrade.wallets.Wallets.update", MagicMock()) wallets_mock = mocker.patch("freqtrade.wallets.Wallets.update", MagicMock())
mocker.patch("freqtrade.wallets.Wallets.get_free", MagicMock(return_value=1))
freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
freqtrade.strategy.order_types['stoploss_on_exchange'] = True freqtrade.strategy.order_types['stoploss_on_exchange'] = True

View File

@ -1,7 +1,8 @@
# pragma pylint: disable=missing-docstring # pragma pylint: disable=missing-docstring
from tests.conftest import get_patched_freqtradebot
from unittest.mock import MagicMock from unittest.mock import MagicMock
from tests.conftest import get_patched_freqtradebot
def test_sync_wallet_at_boot(mocker, default_conf): def test_sync_wallet_at_boot(mocker, default_conf):
default_conf['dry_run'] = False default_conf['dry_run'] = False