Merge remote-tracking branch 'origin/develop' into develop

# Conflicts:
#	config.json.example
#	docs/configuration.md
#	freqtrade/constants.py
#	freqtrade/freqtradebot.py
#	freqtrade/main.py
#	freqtrade/rpc/rpc.py
#	freqtrade/tests/rpc/test_rpc_manager.py
#	freqtrade/tests/rpc/test_rpc_telegram.py
#	requirements.txt
This commit is contained in:
creslinux 2018-07-18 09:04:24 +00:00
commit 6e384fabc7
10 changed files with 167 additions and 361 deletions

View File

@ -6,7 +6,10 @@
"ticker_interval" : "5m", "ticker_interval" : "5m",
"dry_run": false, "dry_run": false,
"trailing_stop": false, "trailing_stop": false,
"unfilledtimeout": 600, "unfilledtimeout": {
"buy": 10,
"sell": 30
},
"bid_strategy": { "bid_strategy": {
"ask_last_balance": 0.0 "ask_last_balance": 0.0
}, },

View File

@ -27,7 +27,8 @@ The table below will list all configuration parameters.
| `stoploss` | -0.10 | No | Value of the stoploss in percent used by the bot. More information below. If set, this parameter will override `stoploss` from your strategy file. | `stoploss` | -0.10 | No | Value of the stoploss in percent used by the bot. More information below. If set, this parameter will override `stoploss` from your strategy file.
| `trailing_stoploss` | false | No | Enables trailing stop-loss (based on `stoploss` in either configuration or strategy file). | `trailing_stoploss` | false | No | Enables trailing stop-loss (based on `stoploss` in either configuration or strategy file).
| `trailing_stoploss_positve` | 0 | No | Changes stop-loss once profit has been reached. | `trailing_stoploss_positve` | 0 | No | Changes stop-loss once profit has been reached.
| `unfilledtimeout` | 0 | No | How long (in minutes) the bot will wait for an unfilled order to complete, after which the order will be cancelled. | `unfilledtimeout.buy` | 10 | Yes | How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled.
| `unfilledtimeout.sell` | 10 | Yes | How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled.
| `bid_strategy.ask_last_balance` | 0.0 | Yes | Set the bidding price. More information below. | `bid_strategy.ask_last_balance` | 0.0 | Yes | Set the bidding price. More information below.
| `exchange.name` | bittrex | Yes | Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename). | `exchange.name` | bittrex | Yes | Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename).
| `exchange.key` | key | No | API key to use for the exchange. Only required when you are in production mode. | `exchange.key` | key | No | API key to use for the exchange. Only required when you are in production mode.

View File

@ -63,7 +63,13 @@ CONF_SCHEMA = {
'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True}, 'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True},
'trailing_stop': {'type': 'boolean'}, 'trailing_stop': {'type': 'boolean'},
'trailing_stop_positive': {'type': 'number', 'minimum': 0, 'maximum': 1}, 'trailing_stop_positive': {'type': 'number', 'minimum': 0, 'maximum': 1},
'unfilledtimeout': {'type': 'integer', 'minimum': 0}, 'unfilledtimeout': {
'type': 'object',
'properties': {
'buy': {'type': 'number', 'minimum': 3},
'sell': {'type': 'number', 'minimum': 10}
}
},
'bid_strategy': { 'bid_strategy': {
'type': 'object', 'type': 'object',
'properties': { 'properties': {

View File

@ -19,8 +19,7 @@ from freqtrade.analyze import Analyze
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.fiat_convert import CryptoToFiatConverter from freqtrade.fiat_convert import CryptoToFiatConverter
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.rpc import RPCMessageType from freqtrade.rpc.rpc_manager import RPCManager
from freqtrade.rpc import RPCManager
from freqtrade.state import State from freqtrade.state import State
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -92,10 +91,7 @@ class FreqtradeBot(object):
# Log state transition # Log state transition
state = self.state state = self.state
if state != old_state: if state != old_state:
self.rpc.send_msg({ self.rpc.send_msg(f'*Status:* `{state.name.lower()}`')
'type': RPCMessageType.STATUS_NOTIFICATION,
'status': f'{state.name.lower()}'
})
logger.info('Changing state to: %s', state.name) logger.info('Changing state to: %s', state.name)
if state == State.STOPPED: if state == State.STOPPED:
@ -171,10 +167,9 @@ class FreqtradeBot(object):
except OperationalException: except OperationalException:
tb = traceback.format_exc() tb = traceback.format_exc()
hint = 'Issue `/start` if you think it is safe to restart.' hint = 'Issue `/start` if you think it is safe to restart.'
self.rpc.send_msg({ self.rpc.send_msg(
'type': RPCMessageType.STATUS_NOTIFICATION, f'*Status:* OperationalException:\n```\n{tb}```{hint}'
'status': f'OperationalException:\n```\n{tb}```{hint}' )
})
logger.exception('OperationalException. Stopping trader ...') logger.exception('OperationalException. Stopping trader ...')
self.state = State.STOPPED self.state = State.STOPPED
return state_changed return state_changed
@ -343,6 +338,7 @@ class FreqtradeBot(object):
pair_url = self.exchange.get_pair_detail_url(pair) pair_url = self.exchange.get_pair_detail_url(pair)
stake_currency = self.config['stake_currency'] stake_currency = self.config['stake_currency']
fiat_currency = self.config['fiat_display_currency'] fiat_currency = self.config['fiat_display_currency']
exc_name = self.exchange.name
# Calculate amount # Calculate amount
buy_limit = self.get_target_bid(self.exchange.get_ticker(pair)) buy_limit = self.get_target_bid(self.exchange.get_ticker(pair))
@ -365,17 +361,12 @@ class FreqtradeBot(object):
fiat_currency fiat_currency
) )
self.rpc.send_msg({ # Create trade entity and return
'type': RPCMessageType.BUY_NOTIFICATION, self.rpc.send_msg(
'exchange': self.exchange.name.capitalize(), f"""*{exc_name}:* Buying [{pair_s}]({pair_url}) \
'pair': pair_s, with limit `{buy_limit:.8f} ({stake_amount:.6f} \
'market_url': pair_url, {stake_currency}, {stake_amount_fiat:.3f} {fiat_currency})`"""
'limit': buy_limit, )
'stake_amount': stake_amount,
'stake_amount_fiat': stake_amount_fiat,
'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(
@ -560,10 +551,7 @@ class FreqtradeBot(object):
Trade.session.delete(trade) Trade.session.delete(trade)
Trade.session.flush() Trade.session.flush()
logger.info('Buy order timeout for %s.', trade) logger.info('Buy order timeout for %s.', trade)
self.rpc.send_msg({ self.rpc.send_msg(f'*Timeout:* Unfilled buy order for {pair_s} cancelled')
'type': RPCMessageType.STATUS_NOTIFICATION,
'status': f'Unfilled buy order for {pair_s} cancelled due to timeout'
})
return True return True
# if trade is partially complete, edit the stake details for the trade # if trade is partially complete, edit the stake details for the trade
@ -572,10 +560,7 @@ class FreqtradeBot(object):
trade.stake_amount = trade.amount * trade.open_rate trade.stake_amount = trade.amount * trade.open_rate
trade.open_order_id = None trade.open_order_id = None
logger.info('Partial buy order timeout for %s.', trade) logger.info('Partial buy order timeout for %s.', trade)
self.rpc.send_msg({ self.rpc.send_msg(f'*Timeout:* Remaining buy order for {pair_s} cancelled')
'type': RPCMessageType.STATUS_NOTIFICATION,
'status': f'Remaining buy order for {pair_s} cancelled due to timeout'
})
return False return False
# FIX: 20180110, should cancel_order() be cond. or unconditionally called? # FIX: 20180110, should cancel_order() be cond. or unconditionally called?
@ -593,10 +578,7 @@ class FreqtradeBot(object):
trade.close_date = None trade.close_date = None
trade.is_open = True trade.is_open = True
trade.open_order_id = None trade.open_order_id = None
self.rpc.send_msg({ self.rpc.send_msg(f'*Timeout:* Unfilled sell order for {pair_s} cancelled')
'type': RPCMessageType.STATUS_NOTIFICATION,
'status': f'Unfilled sell order for {pair_s} cancelled due to timeout'
})
logger.info('Sell order timeout for %s.', trade) logger.info('Sell order timeout for %s.', trade)
return True return True
@ -610,47 +592,47 @@ class FreqtradeBot(object):
:param limit: limit rate for the sell order :param limit: limit rate for the sell order
:return: None :return: None
""" """
exc = trade.exchange
pair = trade.pair
# Execute sell and update trade record # Execute sell and update trade record
order_id = self.exchange.sell(str(trade.pair), limit, trade.amount)['id'] order_id = self.exchange.sell(str(trade.pair), limit, trade.amount)['id']
trade.open_order_id = order_id trade.open_order_id = order_id
trade.close_rate_requested = limit trade.close_rate_requested = limit
fmt_exp_profit = round(trade.calc_profit_percent(rate=limit) * 100, 2)
profit_trade = trade.calc_profit(rate=limit) profit_trade = trade.calc_profit(rate=limit)
current_rate = self.exchange.get_ticker(trade.pair)['bid'] current_rate = self.exchange.get_ticker(trade.pair)['bid']
profit_percent = trade.calc_profit_percent(limit) profit = trade.calc_profit_percent(limit)
pair_url = self.exchange.get_pair_detail_url(trade.pair) pair_url = self.exchange.get_pair_detail_url(trade.pair)
gain = "profit" if profit_percent > 0 else "loss" gain = "profit" if fmt_exp_profit > 0 else "loss"
msg = { message = f"*{exc}:* Selling\n" \
'type': RPCMessageType.SELL_NOTIFICATION, f"*Current Pair:* [{pair}]({pair_url})\n" \
'exchange': trade.exchange.capitalize(), f"*Limit:* `{limit}`\n" \
'pair': trade.pair, f"*Amount:* `{round(trade.amount, 8)}`\n" \
'gain': gain, f"*Open Rate:* `{trade.open_rate:.8f}`\n" \
'market_url': pair_url, f"*Current Rate:* `{current_rate:.8f}`\n" \
'limit': limit, f"*Profit:* `{round(profit * 100, 2):.2f}%`" \
'amount': trade.amount, ""
'open_rate': trade.open_rate,
'current_rate': current_rate,
'profit_amount': profit_trade,
'profit_percent': profit_percent,
}
# For regular case, when the configuration exists # For regular case, when the configuration exists
if 'stake_currency' in self.config and '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'] stake = self.config['stake_currency']
fiat_currency = self.config['fiat_display_currency'] fiat = self.config['fiat_display_currency']
fiat_converter = CryptoToFiatConverter() fiat_converter = CryptoToFiatConverter()
profit_fiat = fiat_converter.convert_amount( profit_fiat = fiat_converter.convert_amount(
profit_trade, profit_trade,
stake_currency, stake,
fiat_currency, fiat
) )
msg.update({ message += f'` ({gain}: {fmt_exp_profit:.2f}%, {profit_trade:.8f} {stake}`' \
'profit_fiat': profit_fiat, f'` / {profit_fiat:.3f} {fiat})`'\
'stake_currency': stake_currency, ''
'fiat_currency': fiat_currency, # Because telegram._forcesell does not have the configuration
}) # Ignore the FIAT value and does not show the stake_currency as well
else:
gain = "profit" if fmt_exp_profit > 0 else "loss"
message += f'` ({gain}: {fmt_exp_profit:.2f}%, {profit_trade:.8f})`'
# Send the message # Send the message
self.rpc.send_msg(msg) self.rpc.send_msg(message)
Trade.session.flush() Trade.session.flush()

View File

@ -13,7 +13,6 @@ from freqtrade.arguments import Arguments
from freqtrade.configuration import Configuration from freqtrade.configuration import Configuration
from freqtrade.freqtradebot import FreqtradeBot from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.state import State from freqtrade.state import State
from freqtrade.rpc import RPCMessageType
logger = logging.getLogger('freqtrade') logger = logging.getLogger('freqtrade')
@ -60,10 +59,7 @@ def main(sysargv: List[str]) -> None:
logger.exception('Fatal exception!') logger.exception('Fatal exception!')
finally: finally:
if freqtrade: if freqtrade:
freqtrade.rpc.send_msg({ freqtrade.rpc.send_msg('*Status:* `Process died ...`')
'type': RPCMessageType.STATUS_NOTIFICATION,
'status': 'process died'
})
freqtrade.cleanup() freqtrade.cleanup()
sys.exit(return_code) sys.exit(return_code)
@ -77,10 +73,8 @@ def reconfigure(freqtrade: FreqtradeBot, args: Namespace) -> FreqtradeBot:
# Create new instance # Create new instance
freqtrade = FreqtradeBot(Configuration(args).get_config()) freqtrade = FreqtradeBot(Configuration(args).get_config())
freqtrade.rpc.send_msg({ freqtrade.rpc.send_msg(
'type': RPCMessageType.STATUS_NOTIFICATION, '*Status:* `Config reloaded {freqtrade.state.name.lower()}...`')
'status': 'config reloaded'
})
return freqtrade return freqtrade

View File

@ -3,10 +3,9 @@ This module contains class to define a RPC communications
""" """
import logging import logging
from abc import abstractmethod from abc import abstractmethod
from datetime import timedelta, datetime, date from datetime import date, datetime, timedelta
from decimal import Decimal from decimal import Decimal
from enum import Enum from typing import Any, Dict, List, Tuple
from typing import Dict, Any, List
import arrow import arrow
import sqlalchemy as sql import sqlalchemy as sql
@ -20,15 +19,6 @@ from freqtrade.state import State
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class RPCMessageType(Enum):
STATUS_NOTIFICATION = 'status'
BUY_NOTIFICATION = 'buy'
SELL_NOTIFICATION = 'sell'
def __repr__(self):
return self.value
class RPCException(Exception): class RPCException(Exception):
""" """
Should be raised with a rpc-formatted message in an _rpc_* method Should be raised with a rpc-formatted message in an _rpc_* method
@ -36,12 +26,7 @@ class RPCException(Exception):
raise RPCException('*Status:* `no active trade`') raise RPCException('*Status:* `no active trade`')
""" """
def __init__(self, message: str) -> None: pass
super().__init__(self)
self.message = message
def __str__(self):
return self.message
class RPC(object): class RPC(object):
@ -56,20 +41,20 @@ class RPC(object):
""" """
self._freqtrade = freqtrade self._freqtrade = freqtrade
@property
def name(self) -> str:
""" Returns the lowercase name of the implementation """
return self.__class__.__name__.lower()
@abstractmethod @abstractmethod
def cleanup(self) -> None: def cleanup(self) -> None:
""" Cleanup pending module resources """ """ Cleanup pending module resources """
@property
@abstractmethod @abstractmethod
def send_msg(self, msg: Dict[str, str]) -> None: def name(self) -> str:
""" Returns the lowercase name of this module """
@abstractmethod
def send_msg(self, msg: str) -> None:
""" Sends a message to all registered rpc modules """ """ Sends a message to all registered rpc modules """
def _rpc_trade_status(self) -> List[Dict[str, Any]]: def _rpc_trade_status(self) -> List[str]:
""" """
Below follows the RPC backend it is prefixed with rpc_ to raise awareness that it is Below follows the RPC backend it is prefixed with rpc_ to raise awareness that it is
a remotely exposed function a remotely exposed function
@ -77,11 +62,11 @@ class RPC(object):
# Fetch open trade # Fetch open trade
trades = Trade.query.filter(Trade.is_open.is_(True)).all() trades = Trade.query.filter(Trade.is_open.is_(True)).all()
if self._freqtrade.state != State.RUNNING: if self._freqtrade.state != State.RUNNING:
raise RPCException('trader is not running') raise RPCException('*Status:* `trader is not running`')
elif not trades: elif not trades:
raise RPCException('no active trade') raise RPCException('*Status:* `no active trade`')
else: else:
results = [] result = []
for trade in trades: for trade in trades:
order = None order = None
if trade.open_order_id: if trade.open_order_id:
@ -91,29 +76,39 @@ class RPC(object):
current_profit = trade.calc_profit_percent(current_rate) current_profit = trade.calc_profit_percent(current_rate)
fmt_close_profit = (f'{round(trade.close_profit * 100, 2):.2f}%' fmt_close_profit = (f'{round(trade.close_profit * 100, 2):.2f}%'
if trade.close_profit else None) if trade.close_profit else None)
results.append(dict( market_url = self._freqtrade.exchange.get_pair_detail_url(trade.pair)
trade_id=trade.id, trade_date = arrow.get(trade.open_date).humanize()
pair=trade.pair, open_rate = trade.open_rate
market_url=self._freqtrade.exchange.get_pair_detail_url(trade.pair), close_rate = trade.close_rate
date=arrow.get(trade.open_date), amount = round(trade.amount, 8)
open_rate=trade.open_rate, current_profit = round(current_profit * 100, 2)
close_rate=trade.close_rate, open_order = ''
current_rate=current_rate, if order:
amount=round(trade.amount, 8), order_type = order['type']
close_profit=fmt_close_profit, order_side = order['side']
current_profit=round(current_profit * 100, 2), order_rem = order['remaining']
open_order='({} {} rem={:.8f})'.format( open_order = f'({order_type} {order_side} rem={order_rem:.8f})'
order['type'], order['side'], order['remaining']
) if order else None, message = f"*Trade ID:* `{trade.id}`\n" \
)) f"*Current Pair:* [{trade.pair}]({market_url})\n" \
return results f"*Open Since:* `{trade_date}`\n" \
f"*Amount:* `{amount}`\n" \
f"*Open Rate:* `{open_rate:.8f}`\n" \
f"*Close Rate:* `{close_rate}`\n" \
f"*Current Rate:* `{current_rate:.8f}`\n" \
f"*Close Profit:* `{fmt_close_profit}`\n" \
f"*Current Profit:* `{current_profit:.2f}%`\n" \
f"*Open Order:* `{open_order}`"\
result.append(message)
return result
def _rpc_status_table(self) -> DataFrame: def _rpc_status_table(self) -> DataFrame:
trades = Trade.query.filter(Trade.is_open.is_(True)).all() trades = Trade.query.filter(Trade.is_open.is_(True)).all()
if self._freqtrade.state != State.RUNNING: if self._freqtrade.state != State.RUNNING:
raise RPCException('trader is not running') raise RPCException('*Status:* `trader is not running`')
elif not trades: elif not trades:
raise RPCException('no active order') raise RPCException('*Status:* `no active order`')
else: else:
trades_list = [] trades_list = []
for trade in trades: for trade in trades:
@ -139,7 +134,7 @@ class RPC(object):
profit_days: Dict[date, Dict] = {} profit_days: Dict[date, Dict] = {}
if not (isinstance(timescale, int) and timescale > 0): if not (isinstance(timescale, int) and timescale > 0):
raise RPCException('timescale must be an integer greater than 0') raise RPCException('*Daily [n]:* `must be an integer greater than 0`')
fiat = self._freqtrade.fiat_converter fiat = self._freqtrade.fiat_converter
for day in range(0, timescale): for day in range(0, timescale):
@ -219,7 +214,7 @@ class RPC(object):
.order_by(sql.text('profit_sum DESC')).first() .order_by(sql.text('profit_sum DESC')).first()
if not best_pair: if not best_pair:
raise RPCException('no closed trade') raise RPCException('*Status:* `no closed trade`')
bp_pair, bp_rate = best_pair bp_pair, bp_rate = best_pair
@ -227,26 +222,26 @@ class RPC(object):
# doing this will utilize its caching functionallity, instead we reinitialize it here # doing this will utilize its caching functionallity, instead we reinitialize it here
fiat = self._freqtrade.fiat_converter fiat = self._freqtrade.fiat_converter
# Prepare data to display # Prepare data to display
profit_closed_coin_sum = round(sum(profit_closed_coin), 8) profit_closed_coin = round(sum(profit_closed_coin), 8)
profit_closed_percent = round(nan_to_num(mean(profit_closed_percent)) * 100, 2) profit_closed_percent = round(nan_to_num(mean(profit_closed_percent)) * 100, 2)
profit_closed_fiat = fiat.convert_amount( profit_closed_fiat = fiat.convert_amount(
profit_closed_coin_sum, profit_closed_coin,
stake_currency, stake_currency,
fiat_display_currency fiat_display_currency
) )
profit_all_coin_sum = round(sum(profit_all_coin), 8) profit_all_coin = round(sum(profit_all_coin), 8)
profit_all_percent = round(nan_to_num(mean(profit_all_percent)) * 100, 2) profit_all_percent = round(nan_to_num(mean(profit_all_percent)) * 100, 2)
profit_all_fiat = fiat.convert_amount( profit_all_fiat = fiat.convert_amount(
profit_all_coin_sum, profit_all_coin,
stake_currency, stake_currency,
fiat_display_currency fiat_display_currency
) )
num = float(len(durations) or 1) num = float(len(durations) or 1)
return { return {
'profit_closed_coin': profit_closed_coin_sum, 'profit_closed_coin': profit_closed_coin,
'profit_closed_percent': profit_closed_percent, 'profit_closed_percent': profit_closed_percent,
'profit_closed_fiat': profit_closed_fiat, 'profit_closed_fiat': profit_closed_fiat,
'profit_all_coin': profit_all_coin_sum, 'profit_all_coin': profit_all_coin,
'profit_all_percent': profit_all_percent, 'profit_all_percent': profit_all_percent,
'profit_all_fiat': profit_all_fiat, 'profit_all_fiat': profit_all_fiat,
'trade_count': len(trades), 'trade_count': len(trades),
@ -257,7 +252,7 @@ class RPC(object):
'best_rate': round(bp_rate * 100, 2), 'best_rate': round(bp_rate * 100, 2),
} }
def _rpc_balance(self, fiat_display_currency: str) -> Dict: def _rpc_balance(self, fiat_display_currency: str) -> Tuple[List[Dict], float, str, float]:
""" Returns current account balance per crypto """ """ Returns current account balance per crypto """
output = [] output = []
total = 0.0 total = 0.0
@ -274,47 +269,45 @@ class RPC(object):
rate = self._freqtrade.exchange.get_ticker(coin + '/BTC', False)['bid'] rate = self._freqtrade.exchange.get_ticker(coin + '/BTC', False)['bid']
est_btc: float = rate * balance['total'] est_btc: float = rate * balance['total']
total = total + est_btc total = total + est_btc
output.append({ output.append(
'currency': coin, {
'available': balance['free'], 'currency': coin,
'balance': balance['total'], 'available': balance['free'],
'pending': balance['used'], 'balance': balance['total'],
'est_btc': est_btc, 'pending': balance['used'],
}) 'est_btc': est_btc
}
)
if total == 0.0: if total == 0.0:
raise RPCException('all balances are zero') raise RPCException('`All balances are zero.`')
fiat = self._freqtrade.fiat_converter fiat = self._freqtrade.fiat_converter
symbol = fiat_display_currency symbol = fiat_display_currency
value = fiat.convert_amount(total, 'BTC', symbol) value = fiat.convert_amount(total, 'BTC', symbol)
return { return output, total, symbol, value
'currencies': output,
'total': total,
'symbol': symbol,
'value': value,
}
def _rpc_start(self) -> Dict[str, str]: def _rpc_start(self) -> str:
""" Handler for start """ """ Handler for start """
if self._freqtrade.state == State.RUNNING: if self._freqtrade.state == State.RUNNING:
return {'status': 'already running'} return '*Status:* `already running`'
self._freqtrade.state = State.RUNNING self._freqtrade.state = State.RUNNING
return {'status': 'starting trader ...'} return '`Starting trader ...`'
def _rpc_stop(self) -> Dict[str, str]: def _rpc_stop(self) -> str:
""" Handler for stop """ """ Handler for stop """
if self._freqtrade.state == State.RUNNING: if self._freqtrade.state == State.RUNNING:
self._freqtrade.state = State.STOPPED self._freqtrade.state = State.STOPPED
return {'status': 'stopping trader ...'} return '`Stopping trader ...`'
return {'status': 'already stopped'} return '*Status:* `already stopped`'
def _rpc_reload_conf(self) -> Dict[str, str]: def _rpc_reload_conf(self) -> str:
""" Handler for reload_conf. """ """ Handler for reload_conf. """
self._freqtrade.state = State.RELOAD_CONF self._freqtrade.state = State.RELOAD_CONF
return {'status': 'reloading config ...'} return '*Status:* `Reloading config ...`'
# FIX: no test for this!!!!
def _rpc_forcesell(self, trade_id) -> None: def _rpc_forcesell(self, trade_id) -> None:
""" """
Handler for forcesell <id>. Handler for forcesell <id>.
@ -348,7 +341,7 @@ class RPC(object):
# ---- EOF def _exec_forcesell ---- # ---- EOF def _exec_forcesell ----
if self._freqtrade.state != State.RUNNING: if self._freqtrade.state != State.RUNNING:
raise RPCException('trader is not running') raise RPCException('`trader is not running`')
if trade_id == 'all': if trade_id == 'all':
# Execute sell for all open orders # Execute sell for all open orders
@ -365,7 +358,7 @@ class RPC(object):
).first() ).first()
if not trade: if not trade:
logger.warning('forcesell: Invalid argument received') logger.warning('forcesell: Invalid argument received')
raise RPCException('invalid argument') raise RPCException('Invalid argument.')
_exec_forcesell(trade) _exec_forcesell(trade)
Trade.session.flush() Trade.session.flush()
@ -376,7 +369,7 @@ class RPC(object):
Shows a performance statistic from finished trades Shows a performance statistic from finished trades
""" """
if self._freqtrade.state != State.RUNNING: if self._freqtrade.state != State.RUNNING:
raise RPCException('trader is not running') raise RPCException('`trader is not running`')
pair_rates = Trade.session.query(Trade.pair, pair_rates = Trade.session.query(Trade.pair,
sql.func.sum(Trade.close_profit).label('profit_sum'), sql.func.sum(Trade.close_profit).label('profit_sum'),
@ -393,6 +386,6 @@ class RPC(object):
def _rpc_count(self) -> List[Trade]: def _rpc_count(self) -> List[Trade]:
""" Returns the number of trades running """ """ Returns the number of trades running """
if self._freqtrade.state != State.RUNNING: if self._freqtrade.state != State.RUNNING:
raise RPCException('trader is not running') raise RPCException('`trader is not running`')
return Trade.query.filter(Trade.is_open.is_(True)).all() return Trade.query.filter(Trade.is_open.is_(True)).all()

View File

@ -6,8 +6,8 @@ import logging
from copy import deepcopy from copy import deepcopy
from unittest.mock import MagicMock from unittest.mock import MagicMock
from freqtrade.rpc import RPCMessageType, RPCManager from freqtrade.rpc.rpc_manager import RPCManager
from freqtrade.tests.conftest import log_has, get_patched_freqtradebot from freqtrade.tests.conftest import get_patched_freqtradebot, log_has
def test_rpc_manager_object() -> None: def test_rpc_manager_object() -> None:
@ -102,12 +102,9 @@ def test_send_msg_telegram_disabled(mocker, default_conf, caplog) -> None:
freqtradebot = get_patched_freqtradebot(mocker, conf) freqtradebot = get_patched_freqtradebot(mocker, conf)
rpc_manager = RPCManager(freqtradebot) rpc_manager = RPCManager(freqtradebot)
rpc_manager.send_msg({ rpc_manager.send_msg('test')
'type': RPCMessageType.STATUS_NOTIFICATION,
'status': 'test'
})
assert log_has("Sending rpc message: {'type': status, 'status': 'test'}", caplog.record_tuples) assert log_has('Sending rpc message: test', caplog.record_tuples)
assert telegram_mock.call_count == 0 assert telegram_mock.call_count == 0
@ -120,10 +117,7 @@ def test_send_msg_telegram_enabled(mocker, default_conf, caplog) -> None:
freqtradebot = get_patched_freqtradebot(mocker, default_conf) freqtradebot = get_patched_freqtradebot(mocker, default_conf)
rpc_manager = RPCManager(freqtradebot) rpc_manager = RPCManager(freqtradebot)
rpc_manager.send_msg({ rpc_manager.send_msg('test')
'type': RPCMessageType.STATUS_NOTIFICATION,
'status': 'test'
})
assert log_has("Sending rpc message: {'type': status, 'status': 'test'}", caplog.record_tuples) assert log_has('Sending rpc message: test', caplog.record_tuples)
assert telegram_mock.call_count == 1 assert telegram_mock.call_count == 1

View File

@ -9,17 +9,14 @@ import re
from copy import deepcopy from copy import deepcopy
from datetime import datetime from datetime import datetime
from random import randint from random import randint
from unittest.mock import MagicMock, ANY from unittest.mock import MagicMock
import arrow
import pytest
from telegram import Chat, Message, Update from telegram import Chat, Message, Update
from telegram.error import NetworkError from telegram.error import NetworkError
from freqtrade import __version__ from freqtrade import __version__
from freqtrade.freqtradebot import FreqtradeBot from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.rpc import RPCMessageType
from freqtrade.rpc.telegram import Telegram, authorized_only from freqtrade.rpc.telegram import Telegram, authorized_only
from freqtrade.state import State from freqtrade.state import State
from freqtrade.tests.conftest import (get_patched_freqtradebot, log_has, from freqtrade.tests.conftest import (get_patched_freqtradebot, log_has,
@ -200,7 +197,6 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None:
patch_get_signal(mocker, (True, False)) patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
@ -214,19 +210,7 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None:
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
_rpc_trade_status=MagicMock(return_value=[{ _rpc_trade_status=MagicMock(return_value=[1, 2, 3]),
'trade_id': 1,
'pair': 'ETH/BTC',
'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH',
'date': arrow.utcnow(),
'open_rate': 1.099e-05,
'close_rate': None,
'current_rate': 1.098e-05,
'amount': 90.99181074,
'close_profit': None,
'current_profit': -0.59,
'open_order': '(limit buy rem=0.00000000)'
}]),
_status_table=status_table, _status_table=status_table,
_send_msg=msg_mock _send_msg=msg_mock
) )
@ -240,7 +224,7 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None:
freqtradebot.create_trade() freqtradebot.create_trade()
telegram._status(bot=MagicMock(), update=update) telegram._status(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 3
update.message.text = MagicMock() update.message.text = MagicMock()
update.message.text.replace = MagicMock(return_value='table 2 3') update.message.text.replace = MagicMock(return_value='table 2 3')
@ -614,7 +598,7 @@ def test_zero_balance_handle(default_conf, update, mocker) -> None:
telegram._balance(bot=MagicMock(), update=update) telegram._balance(bot=MagicMock(), update=update)
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 'all balances are zero' in result assert '`All balances are zero.`' in result
def test_start_handle(default_conf, update, mocker) -> None: def test_start_handle(default_conf, update, mocker) -> None:
@ -680,7 +664,7 @@ def test_stop_handle(default_conf, update, mocker) -> None:
telegram._stop(bot=MagicMock(), update=update) telegram._stop(bot=MagicMock(), update=update)
assert freqtradebot.state == State.STOPPED assert freqtradebot.state == State.STOPPED
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'stopping trader' in msg_mock.call_args_list[0][0][0] assert 'Stopping trader' in msg_mock.call_args_list[0][0][0]
def test_stop_handle_already_stopped(default_conf, update, mocker) -> None: def test_stop_handle_already_stopped(default_conf, update, mocker) -> None:
@ -724,7 +708,7 @@ def test_reload_conf_handle(default_conf, update, mocker) -> None:
telegram._reload_conf(bot=MagicMock(), update=update) telegram._reload_conf(bot=MagicMock(), update=update)
assert freqtradebot.state == State.RELOAD_CONF assert freqtradebot.state == State.RELOAD_CONF
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'reloading config' in msg_mock.call_args_list[0][0][0] assert 'Reloading config' in msg_mock.call_args_list[0][0][0]
def test_forcesell_handle(default_conf, update, ticker, fee, def test_forcesell_handle(default_conf, update, ticker, fee,
@ -761,23 +745,12 @@ def test_forcesell_handle(default_conf, update, ticker, fee,
telegram._forcesell(bot=MagicMock(), update=update) telegram._forcesell(bot=MagicMock(), update=update)
assert rpc_mock.call_count == 2 assert rpc_mock.call_count == 2
last_msg = rpc_mock.call_args_list[-1][0][0] assert 'Selling' in rpc_mock.call_args_list[-1][0][0]
assert { assert '[ETH/BTC]' in rpc_mock.call_args_list[-1][0][0]
'type': RPCMessageType.SELL_NOTIFICATION, assert 'Amount' in rpc_mock.call_args_list[-1][0][0]
'exchange': 'Bittrex', assert '0.00001172' in rpc_mock.call_args_list[-1][0][0]
'pair': 'ETH/BTC', assert 'profit: 6.11%, 0.00006126' in rpc_mock.call_args_list[-1][0][0]
'gain': 'profit', assert '0.919 USD' in rpc_mock.call_args_list[-1][0][0]
'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH',
'limit': 1.172e-05,
'amount': 90.99181073703367,
'open_rate': 1.099e-05,
'current_rate': 1.172e-05,
'profit_amount': 6.126e-05,
'profit_percent': 0.06110514,
'profit_fiat': 0.9189,
'stake_currency': 'BTC',
'fiat_currency': 'USD',
} == last_msg
def test_forcesell_down_handle(default_conf, update, ticker, fee, def test_forcesell_down_handle(default_conf, update, ticker, fee,
@ -818,24 +791,12 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee,
telegram._forcesell(bot=MagicMock(), update=update) telegram._forcesell(bot=MagicMock(), update=update)
assert rpc_mock.call_count == 2 assert rpc_mock.call_count == 2
assert 'Selling' in rpc_mock.call_args_list[-1][0][0]
last_msg = rpc_mock.call_args_list[-1][0][0] assert '[ETH/BTC]' in rpc_mock.call_args_list[-1][0][0]
assert { assert 'Amount' in rpc_mock.call_args_list[-1][0][0]
'type': RPCMessageType.SELL_NOTIFICATION, assert '0.00001044' in rpc_mock.call_args_list[-1][0][0]
'exchange': 'Bittrex', assert 'loss: -5.48%, -0.00005492' in rpc_mock.call_args_list[-1][0][0]
'pair': 'ETH/BTC', assert '-0.824 USD' in rpc_mock.call_args_list[-1][0][0]
'gain': 'loss',
'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH',
'limit': 1.044e-05,
'amount': 90.99181073703367,
'open_rate': 1.099e-05,
'current_rate': 1.044e-05,
'profit_amount': -5.492e-05,
'profit_percent': -0.05478343,
'profit_fiat': -0.8238000000000001,
'stake_currency': 'BTC',
'fiat_currency': 'USD',
} == last_msg
def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker) -> None: def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker) -> None:
@ -868,23 +829,10 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker
telegram._forcesell(bot=MagicMock(), update=update) telegram._forcesell(bot=MagicMock(), update=update)
assert rpc_mock.call_count == 4 assert rpc_mock.call_count == 4
msg = rpc_mock.call_args_list[0][0][0] for args in rpc_mock.call_args_list:
assert { assert '0.00001098' in args[0][0]
'type': RPCMessageType.SELL_NOTIFICATION, assert 'loss: -0.59%, -0.00000591 BTC' in args[0][0]
'exchange': 'Bittrex', assert '-0.089 USD' in args[0][0]
'pair': 'ETH/BTC',
'gain': 'loss',
'market_url': ANY,
'limit': 1.098e-05,
'amount': 90.99181073703367,
'open_rate': 1.099e-05,
'current_rate': 1.098e-05,
'profit_amount': -5.91e-06,
'profit_percent': -0.00589292,
'profit_fiat': -0.08865,
'stake_currency': 'BTC',
'fiat_currency': 'USD',
} == msg
def test_forcesell_handle_invalid(default_conf, update, mocker) -> None: def test_forcesell_handle_invalid(default_conf, update, mocker) -> None:
@ -918,7 +866,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None:
update.message.text = '/forcesell' update.message.text = '/forcesell'
telegram._forcesell(bot=MagicMock(), update=update) telegram._forcesell(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'invalid argument' in msg_mock.call_args_list[0][0][0] assert 'Invalid argument' in msg_mock.call_args_list[0][0][0]
# Invalid argument # Invalid argument
msg_mock.reset_mock() msg_mock.reset_mock()
@ -926,7 +874,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None:
update.message.text = '/forcesell 123456' update.message.text = '/forcesell 123456'
telegram._forcesell(bot=MagicMock(), update=update) telegram._forcesell(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'invalid argument' in msg_mock.call_args_list[0][0][0] assert 'Invalid argument.' in msg_mock.call_args_list[0][0][0]
def test_performance_handle(default_conf, update, ticker, fee, def test_performance_handle(default_conf, update, ticker, fee,
@ -1078,123 +1026,7 @@ def test_version_handle(default_conf, update, mocker) -> None:
assert '*Version:* `{}`'.format(__version__) in msg_mock.call_args_list[0][0][0] assert '*Version:* `{}`'.format(__version__) in msg_mock.call_args_list[0][0][0]
def test_send_msg_buy_notification(default_conf, mocker) -> None: def test_send_msg(default_conf, mocker) -> None:
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)
telegram.send_msg({
'type': RPCMessageType.BUY_NOTIFICATION,
'exchange': 'Bittrex',
'pair': 'ETH/BTC',
'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH',
'limit': 1.099e-05,
'stake_amount': 0.001,
'stake_amount_fiat': 0.0,
'stake_currency': 'BTC',
'fiat_currency': 'USD'
})
assert msg_mock.call_args[0][0] \
== '*Bittrex:* Buying [ETH/BTC](https://bittrex.com/Market/Index?MarketName=BTC-ETH)\n' \
'with limit `0.00001099\n' \
'(0.001000 BTC,0.000 USD)`'
def test_send_msg_sell_notification(default_conf, mocker) -> None:
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)
telegram.send_msg({
'type': RPCMessageType.SELL_NOTIFICATION,
'exchange': 'Binance',
'pair': 'KEY/ETH',
'gain': 'loss',
'market_url': 'https://www.binance.com/tradeDetail.html?symbol=KEY_ETH',
'limit': 3.201e-05,
'amount': 1333.3333333333335,
'open_rate': 7.5e-05,
'current_rate': 3.201e-05,
'profit_amount': -0.05746268,
'profit_percent': -0.57405275,
'profit_fiat': -24.81204044792,
'stake_currency': 'ETH',
'fiat_currency': 'USD'
})
assert msg_mock.call_args[0][0] \
== '*Binance:* Selling [KEY/ETH]' \
'(https://www.binance.com/tradeDetail.html?symbol=KEY_ETH)\n' \
'*Limit:* `0.00003201`\n' \
'*Amount:* `1333.33333333`\n' \
'*Open Rate:* `0.00007500`\n' \
'*Current Rate:* `0.00003201`\n' \
'*Profit:* `-57.41%`` (loss: -0.05746268 ETH`` / -24.812 USD)`'
msg_mock.reset_mock()
telegram.send_msg({
'type': RPCMessageType.SELL_NOTIFICATION,
'exchange': 'Binance',
'pair': 'KEY/ETH',
'gain': 'loss',
'market_url': 'https://www.binance.com/tradeDetail.html?symbol=KEY_ETH',
'limit': 3.201e-05,
'amount': 1333.3333333333335,
'open_rate': 7.5e-05,
'current_rate': 3.201e-05,
'profit_amount': -0.05746268,
'profit_percent': -0.57405275,
'stake_currency': 'ETH',
})
assert msg_mock.call_args[0][0] \
== '*Binance:* Selling [KEY/ETH]' \
'(https://www.binance.com/tradeDetail.html?symbol=KEY_ETH)\n' \
'*Limit:* `0.00003201`\n' \
'*Amount:* `1333.33333333`\n' \
'*Open Rate:* `0.00007500`\n' \
'*Current Rate:* `0.00003201`\n' \
'*Profit:* `-57.41%`'
def test_send_msg_status_notification(default_conf, mocker) -> None:
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)
telegram.send_msg({
'type': RPCMessageType.STATUS_NOTIFICATION,
'status': 'running'
})
assert msg_mock.call_args[0][0] == '*Status:* `running`'
def test_send_msg_unknown_type(default_conf, mocker) -> None:
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)
with pytest.raises(NotImplementedError, match=r'Unknown message type: None'):
telegram.send_msg({
'type': None,
})
def test__send_msg(default_conf, mocker) -> None:
""" """
Test send_msg() method Test send_msg() method
""" """
@ -1210,7 +1042,7 @@ def test__send_msg(default_conf, mocker) -> None:
assert len(bot.method_calls) == 1 assert len(bot.method_calls) == 1
def test__send_msg_network_error(default_conf, mocker, caplog) -> None: def test_send_msg_network_error(default_conf, mocker, caplog) -> None:
""" """
Test send_msg() method Test send_msg() method
""" """

View File

@ -20,7 +20,8 @@ from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.rpc import RPCMessageType from freqtrade.rpc import RPCMessageType
from freqtrade.state import State from freqtrade.state import State
from freqtrade.tests.conftest import log_has, patch_coinmarketcap, patch_exchange from freqtrade.tests.conftest import (log_has, patch_coinmarketcap,
patch_exchange)
# Functions for recurrent object patching # Functions for recurrent object patching

View File

@ -1,5 +1,5 @@
ccxt==1.16.16 ccxt==1.15.42
SQLAlchemy==1.2.10 SQLAlchemy==1.2.9
python-telegram-bot==10.1.0 python-telegram-bot==10.1.0
arrow==0.12.1 arrow==0.12.1
cachetools==2.1.0 cachetools==2.1.0