Merge branch 'develop' into 'validate_whitelist'

This commit is contained in:
Matthias 2019-03-21 06:22:48 +01:00
commit 35d65bc7d7
16 changed files with 207 additions and 103 deletions

View File

@ -65,35 +65,7 @@ Where `-s TestStrategy` refers to the class name within the strategy file `test_
python3 ./freqtrade/main.py backtesting --export trades python3 ./freqtrade/main.py backtesting --export trades
``` ```
The exported trades can be read using the following code for manual analysis, or can be used by the plotting script `plot_dataframe.py` in the scripts folder. The exported trades can be used for [further analysis](#further-backtest-result-analysis), or can be used by the plotting script `plot_dataframe.py` in the scripts folder.
``` python
import json
from pathlib import Path
import pandas as pd
filename=Path('user_data/backtest_data/backtest-result.json')
with filename.open() as file:
data = json.load(file)
columns = ["pair", "profit", "opents", "closets", "index", "duration",
"open_rate", "close_rate", "open_at_end", "sell_reason"]
df = pd.DataFrame(data, columns=columns)
df['opents'] = pd.to_datetime(df['opents'],
unit='s',
utc=True,
infer_datetime_format=True
)
df['closets'] = pd.to_datetime(df['closets'],
unit='s',
utc=True,
infer_datetime_format=True
)
```
If you have some ideas for interesting / helpful backtest data analysis, feel free to submit a PR so the community can benefit from it.
#### Exporting trades to file specifying a custom filename #### Exporting trades to file specifying a custom filename
@ -265,6 +237,8 @@ df.groupby("pair")["sell_reason"].value_counts()
This will allow you to drill deeper into your backtest results, and perform analysis which would make the regular backtest-output unreadable. This will allow you to drill deeper into your backtest results, and perform analysis which would make the regular backtest-output unreadable.
If you have some ideas for interesting / helpful backtest data analysis ideas, please submit a PR so the community can benefit from it.
## Backtesting multiple strategies ## Backtesting multiple strategies
To backtest multiple strategies, a list of Strategies can be provided. To backtest multiple strategies, a list of Strategies can be provided.

View File

@ -20,6 +20,7 @@ Mandatory Parameters are marked as **Required**.
| `ticker_interval` | [1m, 5m, 15m, 30m, 1h, 1d, ...] | The ticker interval to use (1min, 5 min, 15 min, 30 min, 1 hour or 1 day). Default is 5 minutes. [Strategy Override](#parameters-in-the-strategy). | `ticker_interval` | [1m, 5m, 15m, 30m, 1h, 1d, ...] | The ticker interval to use (1min, 5 min, 15 min, 30 min, 1 hour or 1 day). Default is 5 minutes. [Strategy Override](#parameters-in-the-strategy).
| `fiat_display_currency` | USD | **Required.** Fiat currency used to show your profits. More information below. | `fiat_display_currency` | USD | **Required.** Fiat currency used to show your profits. More information below.
| `dry_run` | true | **Required.** Define if the bot must be in Dry-run or production mode. | `dry_run` | true | **Required.** Define if the bot must be in Dry-run or production mode.
| `dry_run_wallet` | 999.9 | 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.
| `process_only_new_candles` | false | If set to true indicators are processed only once a new candle arrives. 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). | `process_only_new_candles` | false | If set to true indicators are processed only once a new candle arrives. 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).
| `minimal_roi` | See below | Set the threshold in percent the bot will use to sell a trade. More information below. [Strategy Override](#parameters-in-the-strategy). | `minimal_roi` | See below | Set the threshold in percent the bot will use to sell a trade. More information below. [Strategy Override](#parameters-in-the-strategy).
| `stoploss` | -0.10 | Value of the stoploss in percent used by the bot. More information below. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). | `stoploss` | -0.10 | Value of the stoploss in percent used by the bot. More information below. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy).

View File

@ -16,6 +16,7 @@ official commands. You can ask at any moment for help with `/help`.
|----------|---------|-------------| |----------|---------|-------------|
| `/start` | | Starts the trader | `/start` | | Starts the trader
| `/stop` | | Stops the trader | `/stop` | | Stops the trader
| `/stopbuy` | | Stops the trader from opening new trades. Gracefully closes open trades according to their rules.
| `/reload_conf` | | Reloads the configuration file | `/reload_conf` | | Reloads the configuration file
| `/status` | | Lists all open trades | `/status` | | Lists all open trades
| `/status table` | | List all open trades in a table format | `/status table` | | List all open trades in a table format
@ -43,7 +44,21 @@ Below, example of Telegram message you will receive for each command.
> `Stopping trader ...` > `Stopping trader ...`
> **Status:** `stopped` > **Status:** `stopped`
## /status ### /stopbuy
> **status:** `Setting max_open_trades to 0. Run /reload_conf to reset.`
Prevents the bot from opening new trades by temporarily setting "max_open_trades" to 0. Open trades will be handled via their regular rules (ROI / Sell-signal, stoploss, ...).
After this, give the bot time to close off open trades (can be checked via `/status table`).
Once all positions are sold, run `/stop` to completely stop the bot.
`/reload_conf` resets "max_open_trades" to the value set in the configuration and resets this command.
!!! warning:
The stop-buy signal is ONLY active while the bot is running, and is not persisted anyway, so restarting the bot will cause this to reset.
### /status
For each open trade, the bot will send you the following message. For each open trade, the bot will send you the following message.
@ -58,7 +73,7 @@ For each open trade, the bot will send you the following message.
> **Current Profit:** `12.95%` > **Current Profit:** `12.95%`
> **Open Order:** `None` > **Open Order:** `None`
## /status table ### /status table
Return the status of all open trades in a table format. Return the status of all open trades in a table format.
``` ```
@ -68,7 +83,7 @@ Return the status of all open trades in a table format.
123 CVC/BTC 1 h 12.95% 123 CVC/BTC 1 h 12.95%
``` ```
## /count ### /count
Return the number of trades used and available. Return the number of trades used and available.
``` ```
@ -77,7 +92,7 @@ current max
2 10 2 10
``` ```
## /profit ### /profit
Return a summary of your profit/loss and performance. Return a summary of your profit/loss and performance.
@ -94,11 +109,11 @@ Return a summary of your profit/loss and performance.
> **Avg. Duration:** `2:33:45` > **Avg. Duration:** `2:33:45`
> **Best Performing:** `PAY/BTC: 50.23%` > **Best Performing:** `PAY/BTC: 50.23%`
## /forcesell <trade_id> ### /forcesell <trade_id>
> **BITTREX:** Selling BTC/LTC with limit `0.01650000 (profit: ~-4.07%, -0.00008168)` > **BITTREX:** Selling BTC/LTC with limit `0.01650000 (profit: ~-4.07%, -0.00008168)`
## /forcebuy <pair> ### /forcebuy <pair>
> **BITTREX**: Buying ETH/BTC with limit `0.03400000` (`1.000000 ETH`, `225.290 USD`) > **BITTREX**: Buying ETH/BTC with limit `0.03400000` (`1.000000 ETH`, `225.290 USD`)
@ -106,7 +121,7 @@ Note that for this to work, `forcebuy_enable` needs to be set to true.
[More details](configuration.md/#understand-forcebuy_enable) [More details](configuration.md/#understand-forcebuy_enable)
## /performance ### /performance
Return the performance of each crypto-currency the bot has sold. Return the performance of each crypto-currency the bot has sold.
> Performance: > Performance:
@ -117,7 +132,7 @@ Return the performance of each crypto-currency the bot has sold.
> 5. `STORJ/BTC 27.24%` > 5. `STORJ/BTC 27.24%`
> ... > ...
## /balance ### /balance
Return the balance of all crypto-currency your have on the exchange. Return the balance of all crypto-currency your have on the exchange.
@ -131,7 +146,7 @@ Return the balance of all crypto-currency your have on the exchange.
> **Balance:** 86.64180098 > **Balance:** 86.64180098
> **Pending:** 0.0 > **Pending:** 0.0
## /daily <n> ### /daily <n>
Per default `/daily` will return the 7 last days. Per default `/daily` will return the 7 last days.
The example below if for `/daily 3`: The example below if for `/daily 3`:
@ -145,6 +160,6 @@ Day Profit BTC Profit USD
2018-01-01 0.00269130 BTC 34.986 USD 2018-01-01 0.00269130 BTC 34.986 USD
``` ```
## /version ### /version
> **Version:** `0.14.3` > **Version:** `0.14.3`

View File

@ -20,6 +20,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'] AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList']
DRY_RUN_WALLET = 999.9
TICKER_INTERVAL_MINUTES = { TICKER_INTERVAL_MINUTES = {
'1m': 1, '1m': 1,
@ -60,6 +61,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'},
'process_only_new_candles': {'type': 'boolean'}, 'process_only_new_candles': {'type': 'boolean'},
'minimal_roi': { 'minimal_roi': {
'type': 'object', 'type': 'object',

View File

@ -66,7 +66,7 @@ def retrier(f):
class Exchange(object): class Exchange(object):
_conf: Dict = {} _config: Dict = {}
_params: Dict = {} _params: Dict = {}
# Dict to specify which options each exchange implements # Dict to specify which options each exchange implements
@ -82,7 +82,7 @@ class Exchange(object):
it does basic validation whether the specified exchange and pairs are valid. it does basic validation whether the specified exchange and pairs are valid.
:return: None :return: None
""" """
self._conf.update(config) self._config.update(config)
self._cached_ticker: Dict[str, Any] = {} self._cached_ticker: Dict[str, Any] = {}
@ -370,7 +370,7 @@ class Exchange(object):
def buy(self, pair: str, ordertype: str, amount: float, def buy(self, pair: str, ordertype: str, amount: float,
rate: float, time_in_force) -> Dict: rate: float, time_in_force) -> Dict:
if self._conf['dry_run']: if self._config['dry_run']:
dry_order = self.dry_run_order(pair, ordertype, "buy", amount, rate) dry_order = self.dry_run_order(pair, ordertype, "buy", amount, rate)
return dry_order return dry_order
@ -383,7 +383,7 @@ class Exchange(object):
def sell(self, pair: str, ordertype: str, amount: float, def sell(self, pair: str, ordertype: str, amount: float,
rate: float, time_in_force='gtc') -> Dict: rate: float, time_in_force='gtc') -> Dict:
if self._conf['dry_run']: if self._config['dry_run']:
dry_order = self.dry_run_order(pair, ordertype, "sell", amount, rate) dry_order = self.dry_run_order(pair, ordertype, "sell", amount, rate)
return dry_order return dry_order
@ -408,7 +408,7 @@ class Exchange(object):
raise OperationalException( raise OperationalException(
'In stoploss limit order, stop price should be more than limit price') 'In stoploss limit order, stop price should be more than limit price')
if self._conf['dry_run']: if self._config['dry_run']:
dry_order = self.dry_run_order( dry_order = self.dry_run_order(
pair, ordertype, "sell", amount, stop_price) pair, ordertype, "sell", amount, stop_price)
return dry_order return dry_order
@ -423,8 +423,8 @@ class Exchange(object):
@retrier @retrier
def get_balance(self, currency: str) -> float: def get_balance(self, currency: str) -> float:
if self._conf['dry_run']: if self._config['dry_run']:
return 999.9 return constants.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()
@ -436,7 +436,7 @@ class Exchange(object):
@retrier @retrier
def get_balances(self) -> dict: def get_balances(self) -> dict:
if self._conf['dry_run']: if self._config['dry_run']:
return {} return {}
try: try:
@ -607,7 +607,7 @@ class Exchange(object):
@retrier @retrier
def cancel_order(self, order_id: str, pair: str) -> None: def cancel_order(self, order_id: str, pair: str) -> None:
if self._conf['dry_run']: if self._config['dry_run']:
return return
try: try:
@ -623,7 +623,7 @@ class Exchange(object):
@retrier @retrier
def get_order(self, order_id: str, pair: str) -> Dict: def get_order(self, order_id: str, pair: str) -> Dict:
if self._conf['dry_run']: if self._config['dry_run']:
order = self._dry_run_open_orders[order_id] order = self._dry_run_open_orders[order_id]
return order return order
try: try:
@ -660,7 +660,7 @@ class Exchange(object):
@retrier @retrier
def get_trades_for_order(self, order_id: str, pair: str, since: datetime) -> List: def get_trades_for_order(self, order_id: str, pair: str, since: datetime) -> List:
if self._conf['dry_run']: if self._config['dry_run']:
return [] return []
if not self.exchange_has('fetchMyTrades'): if not self.exchange_has('fetchMyTrades'):
return [] return []

View File

@ -63,7 +63,7 @@ class FreqtradeBot(object):
exchange_name = self.config.get('exchange', {}).get('name', 'bittrex').title() exchange_name = self.config.get('exchange', {}).get('name', 'bittrex').title()
self.exchange = ExchangeResolver(exchange_name, self.config).exchange self.exchange = ExchangeResolver(exchange_name, self.config).exchange
self.wallets = Wallets(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
@ -613,6 +613,25 @@ class FreqtradeBot(object):
f"(from {order_amount} to {real_amount}) from Trades") f"(from {order_amount} to {real_amount}) from Trades")
return real_amount return real_amount
def get_sell_rate(self, pair: str, refresh: bool) -> float:
"""
Get sell rate - either using get-ticker bid or first bid based on orderbook
The orderbook portion is only used for rpc messaging, which would otherwise fail
for BitMex (has no bid/ask in get_ticker)
or remain static in any other case since it's not updating.
:return: Bid rate
"""
config_ask_strategy = self.config.get('ask_strategy', {})
if config_ask_strategy.get('use_order_book', False):
logger.debug('Using order book to get sell rate')
order_book = self.exchange.get_order_book(pair, 1)
rate = order_book['bids'][0][0]
else:
rate = self.exchange.get_ticker(pair, refresh)['bid']
return rate
def handle_trade(self, trade: Trade) -> bool: def handle_trade(self, trade: Trade) -> bool:
""" """
Sells the current pair if the threshold is reached and updates the trade record. Sells the current pair if the threshold is reached and updates the trade record.
@ -649,7 +668,7 @@ class FreqtradeBot(object):
else: else:
logger.debug('checking sell') logger.debug('checking sell')
sell_rate = self.exchange.get_ticker(trade.pair)['bid'] sell_rate = self.get_sell_rate(trade.pair, True)
if self.check_sell(trade, sell_rate, buy, sell): if self.check_sell(trade, sell_rate, buy, sell):
return True return True
@ -892,7 +911,8 @@ class FreqtradeBot(object):
""" """
profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested
profit_trade = trade.calc_profit(rate=profit_rate) profit_trade = trade.calc_profit(rate=profit_rate)
current_rate = self.exchange.get_ticker(trade.pair)['bid'] # Use cached ticker here - it was updated seconds ago.
current_rate = self.get_sell_rate(trade.pair, False)
profit_percent = trade.calc_profit_percent(profit_rate) profit_percent = trade.calc_profit_percent(profit_rate)
gain = "profit" if profit_percent > 0 else "loss" gain = "profit" if profit_percent > 0 else "loss"

View File

@ -94,7 +94,7 @@ class RPC(object):
order = self._freqtrade.exchange.get_order(trade.open_order_id, trade.pair) order = self._freqtrade.exchange.get_order(trade.open_order_id, trade.pair)
# calculate profit and send message to user # calculate profit and send message to user
try: try:
current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid'] current_rate = self._freqtrade.get_sell_rate(trade.pair, False)
except DependencyException: except DependencyException:
current_rate = NAN current_rate = NAN
current_profit = trade.calc_profit_percent(current_rate) current_profit = trade.calc_profit_percent(current_rate)
@ -125,7 +125,7 @@ class RPC(object):
for trade in trades: for trade in trades:
# calculate profit and send message to user # calculate profit and send message to user
try: try:
current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid'] current_rate = self._freqtrade.get_sell_rate(trade.pair, False)
except DependencyException: except DependencyException:
current_rate = NAN current_rate = NAN
trade_perc = (100 * trade.calc_profit_percent(current_rate)) trade_perc = (100 * trade.calc_profit_percent(current_rate))
@ -213,7 +213,7 @@ class RPC(object):
else: else:
# Get current rate # Get current rate
try: try:
current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid'] current_rate = self._freqtrade.get_sell_rate(trade.pair, False)
except DependencyException: except DependencyException:
current_rate = NAN current_rate = NAN
profit_percent = trade.calc_profit_percent(rate=current_rate) profit_percent = trade.calc_profit_percent(rate=current_rate)
@ -280,9 +280,9 @@ class RPC(object):
else: else:
try: try:
if coin == 'USDT': if coin == 'USDT':
rate = 1.0 / self._freqtrade.exchange.get_ticker('BTC/USDT', False)['bid'] rate = 1.0 / self._freqtrade.get_sell_rate('BTC/USDT', False)
else: else:
rate = self._freqtrade.exchange.get_ticker(coin + '/BTC', False)['bid'] rate = self._freqtrade.get_sell_rate(coin + '/BTC', False)
except (TemporaryError, DependencyException): except (TemporaryError, DependencyException):
continue continue
est_btc: float = rate * balance['total'] est_btc: float = rate * balance['total']
@ -328,6 +328,16 @@ class RPC(object):
self._freqtrade.state = State.RELOAD_CONF self._freqtrade.state = State.RELOAD_CONF
return {'status': 'reloading config ...'} return {'status': 'reloading config ...'}
def _rpc_stopbuy(self) -> Dict[str, str]:
"""
Handler to stop buying, but handle open trades gracefully.
"""
if self._freqtrade.state == State.RUNNING:
# Set 'max_open_trades' to 0
self._freqtrade.config['max_open_trades'] = 0
return {'status': 'No more buy will occur from now. Run /reload_conf to reset.'}
def _rpc_forcesell(self, trade_id) -> None: def _rpc_forcesell(self, trade_id) -> None:
""" """
Handler for forcesell <id>. Handler for forcesell <id>.
@ -356,7 +366,7 @@ class RPC(object):
return return
# Get current rate and execute sell # Get current rate and execute sell
current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid'] current_rate = self._freqtrade.get_sell_rate(trade.pair, False)
self._freqtrade.execute_sell(trade, current_rate, SellType.FORCE_SELL) self._freqtrade.execute_sell(trade, current_rate, SellType.FORCE_SELL)
# ---- EOF def _exec_forcesell ---- # ---- EOF def _exec_forcesell ----

View File

@ -91,6 +91,7 @@ class Telegram(RPC):
CommandHandler('daily', self._daily), CommandHandler('daily', self._daily),
CommandHandler('count', self._count), CommandHandler('count', self._count),
CommandHandler('reload_conf', self._reload_conf), CommandHandler('reload_conf', self._reload_conf),
CommandHandler('stopbuy', self._stopbuy),
CommandHandler('whitelist', self._whitelist), CommandHandler('whitelist', self._whitelist),
CommandHandler('help', self._help), CommandHandler('help', self._help),
CommandHandler('version', self._version), CommandHandler('version', self._version),
@ -362,6 +363,18 @@ class Telegram(RPC):
msg = self._rpc_reload_conf() msg = self._rpc_reload_conf()
self._send_msg('Status: `{status}`'.format(**msg), bot=bot) self._send_msg('Status: `{status}`'.format(**msg), bot=bot)
@authorized_only
def _stopbuy(self, bot: Bot, update: Update) -> None:
"""
Handler for /stop_buy.
Sets max_open_trades to 0 and gracefully sells all open trades
:param bot: telegram bot
:param update: message update
:return: None
"""
msg = self._rpc_stopbuy()
self._send_msg('Status: `{status}`'.format(**msg), bot=bot)
@authorized_only @authorized_only
def _forcesell(self, bot: Bot, update: Update) -> None: def _forcesell(self, bot: Bot, update: Update) -> None:
""" """
@ -481,6 +494,7 @@ class Telegram(RPC):
"*/count:* `Show number of trades running compared to allowed number of trades`" \ "*/count:* `Show number of trades running compared to allowed number of trades`" \
"\n" \ "\n" \
"*/balance:* `Show account balance per currency`\n" \ "*/balance:* `Show account balance per currency`\n" \
"*/stopbuy:* `Stops buying, but handles open trades gracefully` \n" \
"*/reload_conf:* `Reload configuration file` \n" \ "*/reload_conf:* `Reload configuration file` \n" \
"*/whitelist:* `Show current whitelist` \n" \ "*/whitelist:* `Show current whitelist` \n" \
"*/help:* `This help message`\n" \ "*/help:* `This help message`\n" \

View File

@ -406,6 +406,26 @@ def test_rpc_stop(mocker, default_conf) -> None:
assert freqtradebot.state == State.STOPPED assert freqtradebot.state == State.STOPPED
def test_rpc_stopbuy(mocker, default_conf) -> None:
patch_coinmarketcap(mocker)
patch_exchange(mocker)
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
get_ticker=MagicMock()
)
freqtradebot = FreqtradeBot(default_conf)
patch_get_signal(freqtradebot, (True, False))
rpc = RPC(freqtradebot)
freqtradebot.state = State.RUNNING
assert freqtradebot.config['max_open_trades'] != 0
result = rpc._rpc_stopbuy()
assert {'status': 'No more buy will occur from now. Run /reload_conf to reset.'} == result
assert freqtradebot.config['max_open_trades'] == 0
def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None: def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None:
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
patch_exchange(mocker) patch_exchange(mocker)

View File

@ -74,7 +74,7 @@ def test_init(default_conf, mocker, caplog) -> None:
message_str = "rpc.telegram is listening for following commands: [['status'], ['profit'], " \ message_str = "rpc.telegram is listening for following commands: [['status'], ['profit'], " \
"['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], " \ "['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], " \
"['performance'], ['daily'], ['count'], ['reload_conf'], " \ "['performance'], ['daily'], ['count'], ['reload_conf'], " \
"['whitelist'], ['help'], ['version']]" "['stopbuy'], ['whitelist'], ['help'], ['version']]"
assert log_has(message_str, caplog.record_tuples) assert log_has(message_str, caplog.record_tuples)
@ -662,6 +662,26 @@ def test_stop_handle_already_stopped(default_conf, update, mocker) -> None:
assert 'already stopped' in msg_mock.call_args_list[0][0][0] assert 'already stopped' in msg_mock.call_args_list[0][0][0]
def test_stopbuy_handle(default_conf, update, mocker) -> None:
patch_coinmarketcap(mocker)
msg_mock = MagicMock()
mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(),
_send_msg=msg_mock
)
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
telegram = Telegram(freqtradebot)
assert freqtradebot.config['max_open_trades'] != 0
telegram._stopbuy(bot=MagicMock(), update=update)
assert freqtradebot.config['max_open_trades'] == 0
assert msg_mock.call_count == 1
assert 'No more buy will occur from now. Run /reload_conf to reset.' \
in msg_mock.call_args_list[0][0][0]
def test_reload_conf_handle(default_conf, update, mocker) -> None: def test_reload_conf_handle(default_conf, update, mocker) -> None:
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
msg_mock = MagicMock() msg_mock = MagicMock()

View File

@ -2962,6 +2962,31 @@ def test_order_book_ask_strategy(default_conf, limit_buy_order, limit_sell_order
assert freqtrade.handle_trade(trade) is True assert freqtrade.handle_trade(trade) is True
def test_get_sell_rate(default_conf, mocker, ticker, order_book_l2) -> None:
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
get_order_book=order_book_l2,
get_ticker=ticker,
)
pair = "ETH/BTC"
# Test regular mode
ft = get_patched_freqtradebot(mocker, default_conf)
rate = ft.get_sell_rate(pair, True)
assert isinstance(rate, float)
assert rate == 0.00001098
# Test orderbook mode
default_conf['ask_strategy']['use_order_book'] = True
default_conf['ask_strategy']['order_book_min'] = 1
default_conf['ask_strategy']['order_book_max'] = 2
ft = get_patched_freqtradebot(mocker, default_conf)
rate = ft.get_sell_rate(pair, True)
assert isinstance(rate, float)
assert rate == 0.043936
def test_startup_messages(default_conf, mocker): def test_startup_messages(default_conf, mocker):
default_conf['pairlist'] = {'method': 'VolumePairList', default_conf['pairlist'] = {'method': 'VolumePairList',
'config': {'number_assets': 20} 'config': {'number_assets': 20}

View File

@ -23,13 +23,13 @@ def test_sync_wallet_at_boot(mocker, default_conf):
freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
assert len(freqtrade.wallets.wallets) == 2 assert len(freqtrade.wallets._wallets) == 2
assert freqtrade.wallets.wallets['BNT'].free == 1.0 assert freqtrade.wallets._wallets['BNT'].free == 1.0
assert freqtrade.wallets.wallets['BNT'].used == 2.0 assert freqtrade.wallets._wallets['BNT'].used == 2.0
assert freqtrade.wallets.wallets['BNT'].total == 3.0 assert freqtrade.wallets._wallets['BNT'].total == 3.0
assert freqtrade.wallets.wallets['GAS'].free == 0.260739 assert freqtrade.wallets._wallets['GAS'].free == 0.260739
assert freqtrade.wallets.wallets['GAS'].used == 0.0 assert freqtrade.wallets._wallets['GAS'].used == 0.0
assert freqtrade.wallets.wallets['GAS'].total == 0.260739 assert freqtrade.wallets._wallets['GAS'].total == 0.260739
assert freqtrade.wallets.get_free('BNT') == 1.0 assert freqtrade.wallets.get_free('BNT') == 1.0
mocker.patch.multiple( mocker.patch.multiple(
@ -50,13 +50,13 @@ def test_sync_wallet_at_boot(mocker, default_conf):
freqtrade.wallets.update() freqtrade.wallets.update()
assert len(freqtrade.wallets.wallets) == 2 assert len(freqtrade.wallets._wallets) == 2
assert freqtrade.wallets.wallets['BNT'].free == 1.2 assert freqtrade.wallets._wallets['BNT'].free == 1.2
assert freqtrade.wallets.wallets['BNT'].used == 1.9 assert freqtrade.wallets._wallets['BNT'].used == 1.9
assert freqtrade.wallets.wallets['BNT'].total == 3.5 assert freqtrade.wallets._wallets['BNT'].total == 3.5
assert freqtrade.wallets.wallets['GAS'].free == 0.270739 assert freqtrade.wallets._wallets['GAS'].free == 0.270739
assert freqtrade.wallets.wallets['GAS'].used == 0.1 assert freqtrade.wallets._wallets['GAS'].used == 0.1
assert freqtrade.wallets.wallets['GAS'].total == 0.260439 assert freqtrade.wallets._wallets['GAS'].total == 0.260439
assert freqtrade.wallets.get_free('GAS') == 0.270739 assert freqtrade.wallets.get_free('GAS') == 0.270739
assert freqtrade.wallets.get_used('GAS') == 0.1 assert freqtrade.wallets.get_used('GAS') == 0.1
assert freqtrade.wallets.get_total('GAS') == 0.260439 assert freqtrade.wallets.get_total('GAS') == 0.260439
@ -81,11 +81,11 @@ def test_sync_wallet_missing_data(mocker, default_conf):
freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
assert len(freqtrade.wallets.wallets) == 2 assert len(freqtrade.wallets._wallets) == 2
assert freqtrade.wallets.wallets['BNT'].free == 1.0 assert freqtrade.wallets._wallets['BNT'].free == 1.0
assert freqtrade.wallets.wallets['BNT'].used == 2.0 assert freqtrade.wallets._wallets['BNT'].used == 2.0
assert freqtrade.wallets.wallets['BNT'].total == 3.0 assert freqtrade.wallets._wallets['BNT'].total == 3.0
assert freqtrade.wallets.wallets['GAS'].free == 0.260739 assert freqtrade.wallets._wallets['GAS'].free == 0.260739
assert freqtrade.wallets.wallets['GAS'].used is None assert freqtrade.wallets._wallets['GAS'].used is None
assert freqtrade.wallets.wallets['GAS'].total == 0.260739 assert freqtrade.wallets._wallets['GAS'].total == 0.260739
assert freqtrade.wallets.get_free('GAS') == 0.260739 assert freqtrade.wallets.get_free('GAS') == 0.260739

View File

@ -1,15 +1,16 @@
# pragma pylint: disable=W0603 # pragma pylint: disable=W0603
""" Wallet """ """ Wallet """
import logging import logging
from typing import Dict, Any, NamedTuple from typing import Dict, NamedTuple
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade import constants
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# wallet data structure # wallet data structure
class Wallet(NamedTuple): class Wallet(NamedTuple):
exchange: str
currency: str currency: str
free: float = 0 free: float = 0
used: float = 0 used: float = 0
@ -18,17 +19,19 @@ class Wallet(NamedTuple):
class Wallets(object): class Wallets(object):
def __init__(self, exchange: Exchange) -> None: def __init__(self, config: dict, exchange: Exchange) -> None:
self.exchange = exchange self._config = config
self.wallets: Dict[str, Any] = {} self._exchange = exchange
self._wallets: Dict[str, Wallet] = {}
self.update() self.update()
def get_free(self, currency) -> float: def get_free(self, currency) -> float:
if self.exchange._conf['dry_run']: if self._config['dry_run']:
return 999.9 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
else: else:
@ -36,10 +39,10 @@ class Wallets(object):
def get_used(self, currency) -> float: def get_used(self, currency) -> float:
if self.exchange._conf['dry_run']: if self._config['dry_run']:
return 999.9 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
else: else:
@ -47,25 +50,25 @@ class Wallets(object):
def get_total(self, currency) -> float: def get_total(self, currency) -> float:
if self.exchange._conf['dry_run']: if self._config['dry_run']:
return 999.9 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(self) -> None:
balances = self.exchange.get_balances()
balances = self._exchange.get_balances()
for currency in balances: for currency in balances:
self.wallets[currency] = Wallet( self._wallets[currency] = Wallet(
self.exchange.id,
currency, currency,
balances[currency].get('free', None), balances[currency].get('free', None),
balances[currency].get('used', None), balances[currency].get('used', None),
balances[currency].get('total', None) balances[currency].get('total', None)
) )
logger.info('Wallets synced ...') logger.info('Wallets synced.')

View File

@ -8,5 +8,5 @@ pytest==4.3.1
pytest-mock==1.10.1 pytest-mock==1.10.1
pytest-asyncio==0.10.0 pytest-asyncio==0.10.0
pytest-cov==2.6.1 pytest-cov==2.6.1
coveralls==1.6.0 coveralls==1.7.0
mypy==0.670 mypy==0.670

View File

@ -1,5 +1,5 @@
# Include all requirements to run the bot. # Include all requirements to run the bot.
-r requirements.txt -r requirements.txt
plotly==3.7.0 plotly==3.7.1

View File

@ -1,4 +1,4 @@
ccxt==1.18.368 ccxt==1.18.385
SQLAlchemy==1.3.1 SQLAlchemy==1.3.1
python-telegram-bot==11.1.0 python-telegram-bot==11.1.0
arrow==0.13.1 arrow==0.13.1