Merge branch 'develop' of https://github.com/freqtrade/freqtrade into develop
This commit is contained in:
commit
b2c5277fee
@ -70,6 +70,10 @@ class Exchange(object):
|
|||||||
# Check if all pairs are available
|
# Check if all pairs are available
|
||||||
self.validate_pairs(config['exchange']['pair_whitelist'])
|
self.validate_pairs(config['exchange']['pair_whitelist'])
|
||||||
|
|
||||||
|
if config.get('ticker_interval'):
|
||||||
|
# Check if timeframe is available
|
||||||
|
self.validate_timeframes(config['ticker_interval'])
|
||||||
|
|
||||||
def _init_ccxt(self, exchange_config: dict) -> ccxt.Exchange:
|
def _init_ccxt(self, exchange_config: dict) -> ccxt.Exchange:
|
||||||
"""
|
"""
|
||||||
Initialize ccxt with given config and return valid
|
Initialize ccxt with given config and return valid
|
||||||
@ -128,6 +132,15 @@ class Exchange(object):
|
|||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f'Pair {pair} is not available at {self.name}')
|
f'Pair {pair} is not available at {self.name}')
|
||||||
|
|
||||||
|
def validate_timeframes(self, timeframe: List[str]) -> None:
|
||||||
|
"""
|
||||||
|
Checks if ticker interval from config is a supported timeframe on the exchange
|
||||||
|
"""
|
||||||
|
timeframes = self._api.timeframes
|
||||||
|
if timeframe not in timeframes:
|
||||||
|
raise OperationalException(
|
||||||
|
f'Invalid ticker {timeframe}, this Exchange supports {timeframes}')
|
||||||
|
|
||||||
def exchange_has(self, endpoint: str) -> bool:
|
def exchange_has(self, endpoint: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Checks if exchange implements a specific API endpoint.
|
Checks if exchange implements a specific API endpoint.
|
||||||
|
@ -19,7 +19,8 @@ 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.rpc_manager import RPCManager
|
from freqtrade.rpc import RPCMessageType
|
||||||
|
from freqtrade.rpc import RPCManager
|
||||||
from freqtrade.state import State
|
from freqtrade.state import State
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -91,7 +92,10 @@ 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(f'*Status:* `{state.name.lower()}`')
|
self.rpc.send_msg({
|
||||||
|
'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:
|
||||||
@ -167,9 +171,10 @@ 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({
|
||||||
f'*Status:* OperationalException:\n```\n{tb}```{hint}'
|
'type': RPCMessageType.STATUS_NOTIFICATION,
|
||||||
)
|
'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
|
||||||
@ -243,6 +248,11 @@ class FreqtradeBot(object):
|
|||||||
return ticker['ask'] + balance * (ticker['last'] - ticker['ask'])
|
return ticker['ask'] + balance * (ticker['last'] - ticker['ask'])
|
||||||
|
|
||||||
def _get_trade_stake_amount(self) -> Optional[float]:
|
def _get_trade_stake_amount(self) -> Optional[float]:
|
||||||
|
"""
|
||||||
|
Check if stake amount can be fulfilled with the available balance
|
||||||
|
for the stake currency
|
||||||
|
:return: float: Stake Amount
|
||||||
|
"""
|
||||||
stake_amount = self.config['stake_amount']
|
stake_amount = self.config['stake_amount']
|
||||||
avaliable_amount = self.exchange.get_balance(self.config['stake_currency'])
|
avaliable_amount = self.exchange.get_balance(self.config['stake_currency'])
|
||||||
|
|
||||||
@ -338,7 +348,6 @@ 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))
|
||||||
@ -361,12 +370,17 @@ class FreqtradeBot(object):
|
|||||||
fiat_currency
|
fiat_currency
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create trade entity and return
|
self.rpc.send_msg({
|
||||||
self.rpc.send_msg(
|
'type': RPCMessageType.BUY_NOTIFICATION,
|
||||||
f"""*{exc_name}:* Buying [{pair_s}]({pair_url}) \
|
'exchange': self.exchange.name.capitalize(),
|
||||||
with limit `{buy_limit:.8f} ({stake_amount:.6f} \
|
'pair': pair_s,
|
||||||
{stake_currency}, {stake_amount_fiat:.3f} {fiat_currency})`"""
|
'market_url': pair_url,
|
||||||
)
|
'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(
|
||||||
@ -551,7 +565,10 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \
|
|||||||
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(f'*Timeout:* Unfilled buy order for {pair_s} cancelled')
|
self.rpc.send_msg({
|
||||||
|
'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
|
||||||
@ -560,7 +577,10 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \
|
|||||||
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(f'*Timeout:* Remaining buy order for {pair_s} cancelled')
|
self.rpc.send_msg({
|
||||||
|
'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?
|
||||||
@ -578,7 +598,10 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \
|
|||||||
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(f'*Timeout:* Unfilled sell order for {pair_s} cancelled')
|
self.rpc.send_msg({
|
||||||
|
'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
|
||||||
|
|
||||||
@ -592,47 +615,47 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \
|
|||||||
: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 = trade.calc_profit_percent(limit)
|
profit_percent = 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 fmt_exp_profit > 0 else "loss"
|
gain = "profit" if profit_percent > 0 else "loss"
|
||||||
|
|
||||||
message = f"*{exc}:* Selling\n" \
|
msg = {
|
||||||
f"*Current Pair:* [{pair}]({pair_url})\n" \
|
'type': RPCMessageType.SELL_NOTIFICATION,
|
||||||
f"*Limit:* `{limit}`\n" \
|
'exchange': trade.exchange.capitalize(),
|
||||||
f"*Amount:* `{round(trade.amount, 8)}`\n" \
|
'pair': trade.pair,
|
||||||
f"*Open Rate:* `{trade.open_rate:.8f}`\n" \
|
'gain': gain,
|
||||||
f"*Current Rate:* `{current_rate:.8f}`\n" \
|
'market_url': pair_url,
|
||||||
f"*Profit:* `{round(profit * 100, 2):.2f}%`" \
|
'limit': limit,
|
||||||
""
|
'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 = self.config['stake_currency']
|
stake_currency = self.config['stake_currency']
|
||||||
fiat = self.config['fiat_display_currency']
|
fiat_currency = 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,
|
stake_currency,
|
||||||
fiat
|
fiat_currency,
|
||||||
)
|
)
|
||||||
message += f'` ({gain}: {fmt_exp_profit:.2f}%, {profit_trade:.8f} {stake}`' \
|
msg.update({
|
||||||
f'` / {profit_fiat:.3f} {fiat})`'\
|
'profit_fiat': profit_fiat,
|
||||||
''
|
'stake_currency': stake_currency,
|
||||||
# Because telegram._forcesell does not have the configuration
|
'fiat_currency': fiat_currency,
|
||||||
# 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(message)
|
self.rpc.send_msg(msg)
|
||||||
Trade.session.flush()
|
Trade.session.flush()
|
||||||
|
@ -13,6 +13,7 @@ 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')
|
||||||
|
|
||||||
@ -59,7 +60,10 @@ def main(sysargv: List[str]) -> None:
|
|||||||
logger.exception('Fatal exception!')
|
logger.exception('Fatal exception!')
|
||||||
finally:
|
finally:
|
||||||
if freqtrade:
|
if freqtrade:
|
||||||
freqtrade.rpc.send_msg('*Status:* `Process died ...`')
|
freqtrade.rpc.send_msg({
|
||||||
|
'type': RPCMessageType.STATUS_NOTIFICATION,
|
||||||
|
'status': 'process died'
|
||||||
|
})
|
||||||
freqtrade.cleanup()
|
freqtrade.cleanup()
|
||||||
sys.exit(return_code)
|
sys.exit(return_code)
|
||||||
|
|
||||||
@ -73,8 +77,10 @@ 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({
|
||||||
'*Status:* `Config reloaded {freqtrade.state.name.lower()}...`')
|
'type': RPCMessageType.STATUS_NOTIFICATION,
|
||||||
|
'status': 'config reloaded'
|
||||||
|
})
|
||||||
return freqtrade
|
return freqtrade
|
||||||
|
|
||||||
|
|
||||||
|
@ -3,9 +3,10 @@ This module contains class to define a RPC communications
|
|||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
from datetime import date, datetime, timedelta
|
from datetime import timedelta, datetime, date
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import Any, Dict, List, Tuple
|
from enum import Enum
|
||||||
|
from typing import Dict, Any, List
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import sqlalchemy as sql
|
import sqlalchemy as sql
|
||||||
@ -19,6 +20,15 @@ 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
|
||||||
@ -26,7 +36,12 @@ class RPCException(Exception):
|
|||||||
|
|
||||||
raise RPCException('*Status:* `no active trade`')
|
raise RPCException('*Status:* `no active trade`')
|
||||||
"""
|
"""
|
||||||
pass
|
def __init__(self, message: str) -> None:
|
||||||
|
super().__init__(self)
|
||||||
|
self.message = message
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.message
|
||||||
|
|
||||||
|
|
||||||
class RPC(object):
|
class RPC(object):
|
||||||
@ -41,20 +56,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 name(self) -> str:
|
def send_msg(self, msg: Dict[str, str]) -> None:
|
||||||
""" 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[str]:
|
def _rpc_trade_status(self) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
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
|
||||||
@ -62,11 +77,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('*Status:* `trader is not running`')
|
raise RPCException('trader is not running')
|
||||||
elif not trades:
|
elif not trades:
|
||||||
raise RPCException('*Status:* `no active trade`')
|
raise RPCException('no active trade')
|
||||||
else:
|
else:
|
||||||
result = []
|
results = []
|
||||||
for trade in trades:
|
for trade in trades:
|
||||||
order = None
|
order = None
|
||||||
if trade.open_order_id:
|
if trade.open_order_id:
|
||||||
@ -76,39 +91,29 @@ 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)
|
||||||
market_url = self._freqtrade.exchange.get_pair_detail_url(trade.pair)
|
results.append(dict(
|
||||||
trade_date = arrow.get(trade.open_date).humanize()
|
trade_id=trade.id,
|
||||||
open_rate = trade.open_rate
|
pair=trade.pair,
|
||||||
close_rate = trade.close_rate
|
market_url=self._freqtrade.exchange.get_pair_detail_url(trade.pair),
|
||||||
amount = round(trade.amount, 8)
|
date=arrow.get(trade.open_date),
|
||||||
current_profit = round(current_profit * 100, 2)
|
open_rate=trade.open_rate,
|
||||||
open_order = ''
|
close_rate=trade.close_rate,
|
||||||
if order:
|
current_rate=current_rate,
|
||||||
order_type = order['type']
|
amount=round(trade.amount, 8),
|
||||||
order_side = order['side']
|
close_profit=fmt_close_profit,
|
||||||
order_rem = order['remaining']
|
current_profit=round(current_profit * 100, 2),
|
||||||
open_order = f'({order_type} {order_side} rem={order_rem:.8f})'
|
open_order='({} {} rem={:.8f})'.format(
|
||||||
|
order['type'], order['side'], order['remaining']
|
||||||
message = f"*Trade ID:* `{trade.id}`\n" \
|
) if order else None,
|
||||||
f"*Current Pair:* [{trade.pair}]({market_url})\n" \
|
))
|
||||||
f"*Open Since:* `{trade_date}`\n" \
|
return results
|
||||||
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('*Status:* `trader is not running`')
|
raise RPCException('trader is not running')
|
||||||
elif not trades:
|
elif not trades:
|
||||||
raise RPCException('*Status:* `no active order`')
|
raise RPCException('no active order')
|
||||||
else:
|
else:
|
||||||
trades_list = []
|
trades_list = []
|
||||||
for trade in trades:
|
for trade in trades:
|
||||||
@ -134,7 +139,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('*Daily [n]:* `must be an integer greater than 0`')
|
raise RPCException('timescale 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):
|
||||||
@ -214,7 +219,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('*Status:* `no closed trade`')
|
raise RPCException('no closed trade')
|
||||||
|
|
||||||
bp_pair, bp_rate = best_pair
|
bp_pair, bp_rate = best_pair
|
||||||
|
|
||||||
@ -222,26 +227,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 = round(sum(profit_closed_coin), 8)
|
profit_closed_coin_sum = 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,
|
profit_closed_coin_sum,
|
||||||
stake_currency,
|
stake_currency,
|
||||||
fiat_display_currency
|
fiat_display_currency
|
||||||
)
|
)
|
||||||
profit_all_coin = round(sum(profit_all_coin), 8)
|
profit_all_coin_sum = 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,
|
profit_all_coin_sum,
|
||||||
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,
|
'profit_closed_coin': profit_closed_coin_sum,
|
||||||
'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,
|
'profit_all_coin': profit_all_coin_sum,
|
||||||
'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),
|
||||||
@ -252,7 +257,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) -> Tuple[List[Dict], float, str, float]:
|
def _rpc_balance(self, fiat_display_currency: str) -> Dict:
|
||||||
""" Returns current account balance per crypto """
|
""" Returns current account balance per crypto """
|
||||||
output = []
|
output = []
|
||||||
total = 0.0
|
total = 0.0
|
||||||
@ -269,45 +274,47 @@ 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,
|
'currency': coin,
|
||||||
'available': balance['free'],
|
'available': balance['free'],
|
||||||
'balance': balance['total'],
|
'balance': balance['total'],
|
||||||
'pending': balance['used'],
|
'pending': balance['used'],
|
||||||
'est_btc': est_btc
|
'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 output, total, symbol, value
|
return {
|
||||||
|
'currencies': output,
|
||||||
|
'total': total,
|
||||||
|
'symbol': symbol,
|
||||||
|
'value': value,
|
||||||
|
}
|
||||||
|
|
||||||
def _rpc_start(self) -> str:
|
def _rpc_start(self) -> Dict[str, 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 '`Starting trader ...`'
|
return {'status': 'starting trader ...'}
|
||||||
|
|
||||||
def _rpc_stop(self) -> str:
|
def _rpc_stop(self) -> Dict[str, 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 '`Stopping trader ...`'
|
return {'status': 'stopping trader ...'}
|
||||||
|
|
||||||
return '*Status:* `already stopped`'
|
return {'status': 'already stopped'}
|
||||||
|
|
||||||
def _rpc_reload_conf(self) -> str:
|
def _rpc_reload_conf(self) -> Dict[str, 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>.
|
||||||
@ -341,7 +348,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
|
||||||
@ -358,7 +365,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()
|
||||||
@ -369,7 +376,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'),
|
||||||
@ -386,6 +393,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()
|
||||||
|
@ -41,12 +41,16 @@ class StrategyResolver(object):
|
|||||||
if 'minimal_roi' in config:
|
if 'minimal_roi' in config:
|
||||||
self.strategy.minimal_roi = config['minimal_roi']
|
self.strategy.minimal_roi = config['minimal_roi']
|
||||||
logger.info("Override strategy \'minimal_roi\' with value in config file.")
|
logger.info("Override strategy \'minimal_roi\' with value in config file.")
|
||||||
|
else:
|
||||||
|
config['minimal_roi'] = self.strategy.minimal_roi
|
||||||
|
|
||||||
if 'stoploss' in config:
|
if 'stoploss' in config:
|
||||||
self.strategy.stoploss = config['stoploss']
|
self.strategy.stoploss = config['stoploss']
|
||||||
logger.info(
|
logger.info(
|
||||||
"Override strategy \'stoploss\' with value in config file: %s.", config['stoploss']
|
"Override strategy \'stoploss\' with value in config file: %s.", config['stoploss']
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
config['stoploss'] = self.strategy.stoploss
|
||||||
|
|
||||||
if 'ticker_interval' in config:
|
if 'ticker_interval' in config:
|
||||||
self.strategy.ticker_interval = config['ticker_interval']
|
self.strategy.ticker_interval = config['ticker_interval']
|
||||||
@ -54,6 +58,8 @@ class StrategyResolver(object):
|
|||||||
"Override strategy \'ticker_interval\' with value in config file: %s.",
|
"Override strategy \'ticker_interval\' with value in config file: %s.",
|
||||||
config['ticker_interval']
|
config['ticker_interval']
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
config['ticker_interval'] = self.strategy.ticker_interval
|
||||||
|
|
||||||
# Sort and apply type conversions
|
# Sort and apply type conversions
|
||||||
self.strategy.minimal_roi = OrderedDict(sorted(
|
self.strategy.minimal_roi = OrderedDict(sorted(
|
||||||
|
@ -29,6 +29,7 @@ def log_has(line, logs):
|
|||||||
|
|
||||||
def patch_exchange(mocker, api_mock=None) -> None:
|
def patch_exchange(mocker, api_mock=None) -> None:
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||||
if api_mock:
|
if api_mock:
|
||||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
else:
|
else:
|
||||||
|
@ -61,6 +61,7 @@ def test_validate_pairs(default_conf, mocker):
|
|||||||
type(api_mock).id = id_mock
|
type(api_mock).id = id_mock
|
||||||
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||||
Exchange(default_conf)
|
Exchange(default_conf)
|
||||||
|
|
||||||
|
|
||||||
@ -68,6 +69,7 @@ def test_validate_pairs_not_available(default_conf, mocker):
|
|||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
api_mock.load_markets = MagicMock(return_value={})
|
api_mock.load_markets = MagicMock(return_value={})
|
||||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||||
|
|
||||||
with pytest.raises(OperationalException, match=r'not available'):
|
with pytest.raises(OperationalException, match=r'not available'):
|
||||||
Exchange(default_conf)
|
Exchange(default_conf)
|
||||||
@ -81,7 +83,7 @@ def test_validate_pairs_not_compatible(default_conf, mocker):
|
|||||||
conf = deepcopy(default_conf)
|
conf = deepcopy(default_conf)
|
||||||
conf['stake_currency'] = 'ETH'
|
conf['stake_currency'] = 'ETH'
|
||||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||||
with pytest.raises(OperationalException, match=r'not compatible'):
|
with pytest.raises(OperationalException, match=r'not compatible'):
|
||||||
Exchange(conf)
|
Exchange(conf)
|
||||||
|
|
||||||
@ -93,6 +95,7 @@ def test_validate_pairs_exception(default_conf, mocker, caplog):
|
|||||||
|
|
||||||
api_mock.load_markets = MagicMock(return_value={})
|
api_mock.load_markets = MagicMock(return_value={})
|
||||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock)
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||||
|
|
||||||
with pytest.raises(OperationalException, match=r'Pair ETH/BTC is not available at Binance'):
|
with pytest.raises(OperationalException, match=r'Pair ETH/BTC is not available at Binance'):
|
||||||
Exchange(default_conf)
|
Exchange(default_conf)
|
||||||
@ -112,6 +115,7 @@ def test_validate_pairs_stake_exception(default_conf, mocker, caplog):
|
|||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
api_mock.name = MagicMock(return_value='binance')
|
api_mock.name = MagicMock(return_value='binance')
|
||||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock)
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||||
|
|
||||||
with pytest.raises(
|
with pytest.raises(
|
||||||
OperationalException,
|
OperationalException,
|
||||||
@ -120,6 +124,55 @@ def test_validate_pairs_stake_exception(default_conf, mocker, caplog):
|
|||||||
Exchange(conf)
|
Exchange(conf)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_timeframes(default_conf, mocker):
|
||||||
|
default_conf["ticker_interval"] = "5m"
|
||||||
|
api_mock = MagicMock()
|
||||||
|
id_mock = PropertyMock(return_value='test_exchange')
|
||||||
|
type(api_mock).id = id_mock
|
||||||
|
timeframes = PropertyMock(return_value={'1m': '1m',
|
||||||
|
'5m': '5m',
|
||||||
|
'15m': '15m',
|
||||||
|
'1h': '1h'})
|
||||||
|
type(api_mock).timeframes = timeframes
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
|
||||||
|
Exchange(default_conf)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_timeframes_failed(default_conf, mocker):
|
||||||
|
default_conf["ticker_interval"] = "3m"
|
||||||
|
api_mock = MagicMock()
|
||||||
|
id_mock = PropertyMock(return_value='test_exchange')
|
||||||
|
type(api_mock).id = id_mock
|
||||||
|
timeframes = PropertyMock(return_value={'1m': '1m',
|
||||||
|
'5m': '5m',
|
||||||
|
'15m': '15m',
|
||||||
|
'1h': '1h'})
|
||||||
|
type(api_mock).timeframes = timeframes
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
|
||||||
|
with pytest.raises(OperationalException, match=r'Invalid ticker 3m, this Exchange supports.*'):
|
||||||
|
Exchange(default_conf)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_timeframes_not_in_config(default_conf, mocker):
|
||||||
|
del default_conf["ticker_interval"]
|
||||||
|
api_mock = MagicMock()
|
||||||
|
id_mock = PropertyMock(return_value='test_exchange')
|
||||||
|
type(api_mock).id = id_mock
|
||||||
|
timeframes = PropertyMock(return_value={'1m': '1m',
|
||||||
|
'5m': '5m',
|
||||||
|
'15m': '15m',
|
||||||
|
'1h': '1h'})
|
||||||
|
type(api_mock).timeframes = timeframes
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
|
||||||
|
Exchange(default_conf)
|
||||||
|
|
||||||
|
|
||||||
def test_exchangehas(default_conf, mocker):
|
def test_exchangehas(default_conf, mocker):
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
assert not exchange.exchange_has('ASDFASDF')
|
assert not exchange.exchange_has('ASDFASDF')
|
||||||
|
@ -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.rpc_manager import RPCManager
|
from freqtrade.rpc import RPCMessageType, RPCManager
|
||||||
from freqtrade.tests.conftest import get_patched_freqtradebot, log_has
|
from freqtrade.tests.conftest import log_has, get_patched_freqtradebot
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_manager_object() -> None:
|
def test_rpc_manager_object() -> None:
|
||||||
@ -102,9 +102,12 @@ 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('test')
|
rpc_manager.send_msg({
|
||||||
|
'type': RPCMessageType.STATUS_NOTIFICATION,
|
||||||
|
'status': 'test'
|
||||||
|
})
|
||||||
|
|
||||||
assert log_has('Sending rpc message: test', caplog.record_tuples)
|
assert log_has("Sending rpc message: {'type': status, 'status': 'test'}", caplog.record_tuples)
|
||||||
assert telegram_mock.call_count == 0
|
assert telegram_mock.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
@ -117,7 +120,10 @@ 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('test')
|
rpc_manager.send_msg({
|
||||||
|
'type': RPCMessageType.STATUS_NOTIFICATION,
|
||||||
|
'status': 'test'
|
||||||
|
})
|
||||||
|
|
||||||
assert log_has('Sending rpc message: test', caplog.record_tuples)
|
assert log_has("Sending rpc message: {'type': status, 'status': 'test'}", caplog.record_tuples)
|
||||||
assert telegram_mock.call_count == 1
|
assert telegram_mock.call_count == 1
|
||||||
|
@ -9,14 +9,17 @@ 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
|
from unittest.mock import MagicMock, ANY
|
||||||
|
|
||||||
|
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,
|
||||||
@ -197,6 +200,7 @@ 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(),
|
||||||
@ -210,7 +214,19 @@ 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=[1, 2, 3]),
|
_rpc_trade_status=MagicMock(return_value=[{
|
||||||
|
'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
|
||||||
)
|
)
|
||||||
@ -224,7 +240,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 == 3
|
assert msg_mock.call_count == 1
|
||||||
|
|
||||||
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')
|
||||||
@ -598,7 +614,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:
|
||||||
@ -664,7 +680,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:
|
||||||
@ -708,7 +724,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,
|
||||||
@ -745,12 +761,23 @@ 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
|
||||||
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.00001172' in rpc_mock.call_args_list[-1][0][0]
|
'exchange': 'Bittrex',
|
||||||
assert 'profit: 6.11%, 0.00006126' in rpc_mock.call_args_list[-1][0][0]
|
'pair': 'ETH/BTC',
|
||||||
assert '0.919 USD' in rpc_mock.call_args_list[-1][0][0]
|
'gain': 'profit',
|
||||||
|
'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,
|
||||||
@ -791,12 +818,24 @@ 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]
|
|
||||||
assert '[ETH/BTC]' in rpc_mock.call_args_list[-1][0][0]
|
last_msg = rpc_mock.call_args_list[-1][0][0]
|
||||||
assert 'Amount' in rpc_mock.call_args_list[-1][0][0]
|
assert {
|
||||||
assert '0.00001044' in rpc_mock.call_args_list[-1][0][0]
|
'type': RPCMessageType.SELL_NOTIFICATION,
|
||||||
assert 'loss: -5.48%, -0.00005492' in rpc_mock.call_args_list[-1][0][0]
|
'exchange': 'Bittrex',
|
||||||
assert '-0.824 USD' in rpc_mock.call_args_list[-1][0][0]
|
'pair': 'ETH/BTC',
|
||||||
|
'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:
|
||||||
@ -829,10 +868,23 @@ 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
|
||||||
for args in rpc_mock.call_args_list:
|
msg = rpc_mock.call_args_list[0][0][0]
|
||||||
assert '0.00001098' in args[0][0]
|
assert {
|
||||||
assert 'loss: -0.59%, -0.00000591 BTC' in args[0][0]
|
'type': RPCMessageType.SELL_NOTIFICATION,
|
||||||
assert '-0.089 USD' in args[0][0]
|
'exchange': 'Bittrex',
|
||||||
|
'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:
|
||||||
@ -866,7 +918,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()
|
||||||
@ -874,7 +926,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,
|
||||||
@ -1026,7 +1078,123 @@ 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(default_conf, mocker) -> None:
|
def test_send_msg_buy_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.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
|
||||||
"""
|
"""
|
||||||
@ -1042,7 +1210,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
|
||||||
"""
|
"""
|
||||||
|
@ -74,13 +74,21 @@ def test_load_not_found_strategy():
|
|||||||
|
|
||||||
|
|
||||||
def test_strategy(result):
|
def test_strategy(result):
|
||||||
resolver = StrategyResolver({'strategy': 'DefaultStrategy'})
|
config = {'strategy': 'DefaultStrategy'}
|
||||||
|
|
||||||
|
resolver = StrategyResolver(config)
|
||||||
|
|
||||||
assert hasattr(resolver.strategy, 'minimal_roi')
|
assert hasattr(resolver.strategy, 'minimal_roi')
|
||||||
assert resolver.strategy.minimal_roi[0] == 0.04
|
assert resolver.strategy.minimal_roi[0] == 0.04
|
||||||
|
assert config["minimal_roi"]['0'] == 0.04
|
||||||
|
|
||||||
assert hasattr(resolver.strategy, 'stoploss')
|
assert hasattr(resolver.strategy, 'stoploss')
|
||||||
assert resolver.strategy.stoploss == -0.10
|
assert resolver.strategy.stoploss == -0.10
|
||||||
|
assert config['stoploss'] == -0.10
|
||||||
|
|
||||||
|
assert hasattr(resolver.strategy, 'ticker_interval')
|
||||||
|
assert resolver.strategy.ticker_interval == '5m'
|
||||||
|
assert config['ticker_interval'] == '5m'
|
||||||
|
|
||||||
assert hasattr(resolver.strategy, 'populate_indicators')
|
assert hasattr(resolver.strategy, 'populate_indicators')
|
||||||
assert 'adx' in resolver.strategy.populate_indicators(result)
|
assert 'adx' in resolver.strategy.populate_indicators(result)
|
||||||
|
@ -20,8 +20,7 @@ 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,
|
from freqtrade.tests.conftest import log_has, patch_coinmarketcap, patch_exchange
|
||||||
patch_exchange)
|
|
||||||
|
|
||||||
|
|
||||||
# Functions for recurrent object patching
|
# Functions for recurrent object patching
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
ccxt==1.15.42
|
ccxt==1.16.50
|
||||||
SQLAlchemy==1.2.9
|
SQLAlchemy==1.2.10
|
||||||
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
|
||||||
@ -7,7 +7,7 @@ requests==2.19.1
|
|||||||
urllib3==1.22
|
urllib3==1.22
|
||||||
wrapt==1.10.11
|
wrapt==1.10.11
|
||||||
pandas==0.23.3
|
pandas==0.23.3
|
||||||
scikit-learn==0.19.1
|
scikit-learn==0.19.2
|
||||||
scipy==1.1.0
|
scipy==1.1.0
|
||||||
jsonschema==2.6.0
|
jsonschema==2.6.0
|
||||||
numpy==1.14.5
|
numpy==1.14.5
|
||||||
|
Loading…
Reference in New Issue
Block a user