Merge pull request #1269 from freqtrade/feat/force_buy
add /forcebuy to telgram handler
This commit is contained in:
@@ -127,6 +127,9 @@ class Configuration(object):
|
||||
config['db_url'] = constants.DEFAULT_DB_PROD_URL
|
||||
logger.info('Dry run is disabled')
|
||||
|
||||
if config.get('forcebuy_enable', False):
|
||||
logger.warning('`forcebuy` RPC message enabled.')
|
||||
|
||||
logger.info(f'Using DB: "{config["db_url"]}"')
|
||||
|
||||
# Check if the exchange set by the user is supported
|
||||
|
@@ -130,6 +130,7 @@ CONF_SCHEMA = {
|
||||
},
|
||||
'db_url': {'type': 'string'},
|
||||
'initial_state': {'type': 'string', 'enum': ['running', 'stopped']},
|
||||
'forcebuy_enable': {'type': 'boolean'},
|
||||
'internals': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
|
@@ -427,7 +427,7 @@ class FreqtradeBot(object):
|
||||
return True
|
||||
return False
|
||||
|
||||
def execute_buy(self, pair: str, stake_amount: float) -> bool:
|
||||
def execute_buy(self, pair: str, stake_amount: float, price: Optional[float] = None) -> bool:
|
||||
"""
|
||||
Executes a limit buy for the given pair
|
||||
:param pair: pair for which we want to create a LIMIT_BUY
|
||||
@@ -438,8 +438,11 @@ class FreqtradeBot(object):
|
||||
stake_currency = self.config['stake_currency']
|
||||
fiat_currency = self.config.get('fiat_display_currency', None)
|
||||
|
||||
# Calculate amount
|
||||
buy_limit = self.get_target_bid(pair, self.exchange.get_ticker(pair))
|
||||
if price:
|
||||
buy_limit = price
|
||||
else:
|
||||
# Calculate amount
|
||||
buy_limit = self.get_target_bid(pair, self.exchange.get_ticker(pair))
|
||||
|
||||
min_stake_amount = self._get_min_pair_stake_amount(pair_s, buy_limit)
|
||||
if min_stake_amount is not None and min_stake_amount > stake_amount:
|
||||
|
@@ -385,6 +385,40 @@ class RPC(object):
|
||||
_exec_forcesell(trade)
|
||||
Trade.session.flush()
|
||||
|
||||
def _rpc_forcebuy(self, pair: str, price: Optional[float]) -> Optional[Trade]:
|
||||
"""
|
||||
Handler for forcebuy <asset> <price>
|
||||
Buys a pair trade at the given or current price
|
||||
"""
|
||||
|
||||
if not self._freqtrade.config.get('forcebuy_enable', False):
|
||||
raise RPCException('Forcebuy not enabled.')
|
||||
|
||||
if self._freqtrade.state != State.RUNNING:
|
||||
raise RPCException('trader is not running')
|
||||
|
||||
# Check pair is in stake currency
|
||||
stake_currency = self._freqtrade.config.get('stake_currency')
|
||||
if not pair.endswith(stake_currency):
|
||||
raise RPCException(
|
||||
f'Wrong pair selected. Please pairs with stake {stake_currency} pairs only')
|
||||
# check if valid pair
|
||||
|
||||
# check if pair already has an open pair
|
||||
trade = Trade.query.filter(Trade.is_open.is_(True)).filter(Trade.pair.is_(pair)).first()
|
||||
if trade:
|
||||
raise RPCException(f'position for {pair} already open - id: {trade.id}')
|
||||
|
||||
# gen stake amount
|
||||
stakeamount = self._freqtrade._get_trade_stake_amount()
|
||||
|
||||
# execute buy
|
||||
if self._freqtrade.execute_buy(pair, stakeamount, price):
|
||||
trade = Trade.query.filter(Trade.is_open.is_(True)).filter(Trade.pair.is_(pair)).first()
|
||||
return trade
|
||||
else:
|
||||
return None
|
||||
|
||||
def _rpc_performance(self) -> List[Dict]:
|
||||
"""
|
||||
Handler for performance.
|
||||
|
@@ -86,6 +86,7 @@ class Telegram(RPC):
|
||||
CommandHandler('start', self._start),
|
||||
CommandHandler('stop', self._stop),
|
||||
CommandHandler('forcesell', self._forcesell),
|
||||
CommandHandler('forcebuy', self._forcebuy),
|
||||
CommandHandler('performance', self._performance),
|
||||
CommandHandler('daily', self._daily),
|
||||
CommandHandler('count', self._count),
|
||||
@@ -375,6 +376,24 @@ class Telegram(RPC):
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e), bot=bot)
|
||||
|
||||
@authorized_only
|
||||
def _forcebuy(self, bot: Bot, update: Update) -> None:
|
||||
"""
|
||||
Handler for /forcebuy <asset> <price>.
|
||||
Buys a pair trade at the given or current price
|
||||
:param bot: telegram bot
|
||||
:param update: message update
|
||||
:return: None
|
||||
"""
|
||||
|
||||
message = update.message.text.replace('/forcebuy', '').strip().split()
|
||||
pair = message[0]
|
||||
price = float(message[1]) if len(message) > 1 else None
|
||||
try:
|
||||
self._rpc_forcebuy(pair, price)
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e), bot=bot)
|
||||
|
||||
@authorized_only
|
||||
def _performance(self, bot: Bot, update: Update) -> None:
|
||||
"""
|
||||
|
@@ -569,3 +569,79 @@ def test_rpc_count(mocker, default_conf, ticker, fee, markets) -> None:
|
||||
trades = rpc._rpc_count()
|
||||
nb_trades = len(trades)
|
||||
assert nb_trades == 1
|
||||
|
||||
|
||||
def test_rpcforcebuy(mocker, default_conf, ticker, fee, markets, limit_buy_order) -> None:
|
||||
default_conf['forcebuy_enable'] = True
|
||||
patch_coinmarketcap(mocker)
|
||||
patch_exchange(mocker)
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
buy_mm = MagicMock(return_value={'id': limit_buy_order['id']})
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
get_balances=MagicMock(return_value=ticker),
|
||||
get_ticker=ticker,
|
||||
get_fee=fee,
|
||||
get_markets=markets,
|
||||
buy=buy_mm
|
||||
)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
rpc = RPC(freqtradebot)
|
||||
pair = 'ETH/BTC'
|
||||
trade = rpc._rpc_forcebuy(pair, None)
|
||||
assert isinstance(trade, Trade)
|
||||
assert trade.pair == pair
|
||||
assert trade.open_rate == ticker()['ask']
|
||||
|
||||
# Test buy duplicate
|
||||
with pytest.raises(RPCException, match=r'position for ETH/BTC already open - id: 1'):
|
||||
rpc._rpc_forcebuy(pair, 0.0001)
|
||||
pair = 'XRP/BTC'
|
||||
trade = rpc._rpc_forcebuy(pair, 0.0001)
|
||||
assert isinstance(trade, Trade)
|
||||
assert trade.pair == pair
|
||||
assert trade.open_rate == 0.0001
|
||||
|
||||
# Test buy pair not with stakes
|
||||
with pytest.raises(RPCException, match=r'Wrong pair selected. Please pairs with stake.*'):
|
||||
rpc._rpc_forcebuy('XRP/ETH', 0.0001)
|
||||
pair = 'XRP/BTC'
|
||||
|
||||
# Test not buying
|
||||
default_conf['stake_amount'] = 0.0000001
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
rpc = RPC(freqtradebot)
|
||||
pair = 'TKN/BTC'
|
||||
trade = rpc._rpc_forcebuy(pair, None)
|
||||
assert trade is None
|
||||
|
||||
|
||||
def test_rpcforcebuy_stopped(mocker, default_conf) -> None:
|
||||
default_conf['forcebuy_enable'] = True
|
||||
default_conf['initial_state'] = 'stopped'
|
||||
patch_coinmarketcap(mocker)
|
||||
patch_exchange(mocker)
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
rpc = RPC(freqtradebot)
|
||||
pair = 'ETH/BTC'
|
||||
with pytest.raises(RPCException, match=r'trader is not running'):
|
||||
rpc._rpc_forcebuy(pair, None)
|
||||
|
||||
|
||||
def test_rpcforcebuy_disabled(mocker, default_conf) -> None:
|
||||
patch_coinmarketcap(mocker)
|
||||
patch_exchange(mocker)
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
rpc = RPC(freqtradebot)
|
||||
pair = 'ETH/BTC'
|
||||
with pytest.raises(RPCException, match=r'Forcebuy not enabled.'):
|
||||
rpc._rpc_forcebuy(pair, None)
|
||||
|
@@ -71,8 +71,8 @@ def test_init(default_conf, mocker, caplog) -> None:
|
||||
assert start_polling.start_polling.call_count == 1
|
||||
|
||||
message_str = "rpc.telegram is listening for following commands: [['status'], ['profit'], " \
|
||||
"['balance'], ['start'], ['stop'], ['forcesell'], ['performance'], ['daily'], " \
|
||||
"['count'], ['reload_conf'], ['help'], ['version']]"
|
||||
"['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], " \
|
||||
"['performance'], ['daily'], ['count'], ['reload_conf'], ['help'], ['version']]"
|
||||
|
||||
assert log_has(message_str, caplog.record_tuples)
|
||||
|
||||
@@ -868,6 +868,63 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None:
|
||||
assert 'invalid argument' in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
|
||||
def test_forcebuy_handle(default_conf, update, markets, mocker) -> None:
|
||||
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
||||
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram._send_msg', MagicMock())
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
_load_markets=MagicMock(return_value={}),
|
||||
get_markets=markets
|
||||
)
|
||||
fbuy_mock = MagicMock(return_value=None)
|
||||
mocker.patch('freqtrade.rpc.RPC._rpc_forcebuy', fbuy_mock)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
update.message.text = '/forcebuy ETH/BTC'
|
||||
telegram._forcebuy(bot=MagicMock(), update=update)
|
||||
|
||||
assert fbuy_mock.call_count == 1
|
||||
assert fbuy_mock.call_args_list[0][0][0] == 'ETH/BTC'
|
||||
assert fbuy_mock.call_args_list[0][0][1] is None
|
||||
|
||||
# Reset and retry with specified price
|
||||
fbuy_mock = MagicMock(return_value=None)
|
||||
mocker.patch('freqtrade.rpc.RPC._rpc_forcebuy', fbuy_mock)
|
||||
update.message.text = '/forcebuy ETH/BTC 0.055'
|
||||
telegram._forcebuy(bot=MagicMock(), update=update)
|
||||
|
||||
assert fbuy_mock.call_count == 1
|
||||
assert fbuy_mock.call_args_list[0][0][0] == 'ETH/BTC'
|
||||
assert isinstance(fbuy_mock.call_args_list[0][0][1], float)
|
||||
assert fbuy_mock.call_args_list[0][0][1] == 0.055
|
||||
|
||||
|
||||
def test_forcebuy_handle_exception(default_conf, update, markets, mocker) -> None:
|
||||
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
||||
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||
rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram._send_msg', MagicMock())
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
_load_markets=MagicMock(return_value={}),
|
||||
get_markets=markets
|
||||
)
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
telegram = Telegram(freqtradebot)
|
||||
|
||||
update.message.text = '/forcebuy ETH/Nonepair'
|
||||
telegram._forcebuy(bot=MagicMock(), update=update)
|
||||
|
||||
assert rpc_mock.call_count == 1
|
||||
assert rpc_mock.call_args_list[0][0][0] == 'Forcebuy not enabled.'
|
||||
|
||||
|
||||
def test_performance_handle(default_conf, update, ticker, fee,
|
||||
limit_buy_order, limit_sell_order, markets, mocker) -> None:
|
||||
patch_coinmarketcap(mocker)
|
||||
|
@@ -455,5 +455,19 @@ def test_set_loggers() -> None:
|
||||
assert logging.getLogger('telegram').level is logging.INFO
|
||||
|
||||
|
||||
def test_load_config_warn_forcebuy(default_conf, mocker, caplog) -> None:
|
||||
default_conf['forcebuy_enable'] = True
|
||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||
read_data=json.dumps(default_conf)
|
||||
))
|
||||
|
||||
args = Arguments([], '').get_parsed_arg()
|
||||
configuration = Configuration(args)
|
||||
validated_conf = configuration.load_config()
|
||||
|
||||
assert validated_conf.get('forcebuy_enable')
|
||||
assert log_has('`forcebuy` RPC message enabled.', caplog.record_tuples)
|
||||
|
||||
|
||||
def test_validate_default_conf(default_conf) -> None:
|
||||
validate(default_conf, constants.CONF_SCHEMA)
|
||||
|
@@ -730,6 +730,54 @@ def test_balance_bigger_last_ask(mocker, default_conf) -> None:
|
||||
assert freqtrade.get_target_bid('ETH/BTC', {'ask': 5, 'last': 10}) == 5
|
||||
|
||||
|
||||
def test_execute_buy(mocker, default_conf, fee, markets, limit_buy_order) -> None:
|
||||
patch_RPCManager(mocker)
|
||||
patch_exchange(mocker)
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
stake_amount = 2
|
||||
bid = 0.11
|
||||
get_bid = MagicMock(return_value=bid)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.freqtradebot.FreqtradeBot',
|
||||
get_target_bid=get_bid,
|
||||
_get_min_pair_stake_amount=MagicMock(return_value=1)
|
||||
)
|
||||
buy_mm = MagicMock(return_value={'id': limit_buy_order['id']})
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
get_ticker=MagicMock(return_value={
|
||||
'bid': 0.00001172,
|
||||
'ask': 0.00001173,
|
||||
'last': 0.00001172
|
||||
}),
|
||||
buy=buy_mm,
|
||||
get_fee=fee,
|
||||
get_markets=markets
|
||||
)
|
||||
pair = 'ETH/BTC'
|
||||
print(buy_mm.call_args_list)
|
||||
|
||||
assert freqtrade.execute_buy(pair, stake_amount)
|
||||
assert get_bid.call_count == 1
|
||||
assert buy_mm.call_count == 1
|
||||
call_args = buy_mm.call_args_list[0][0]
|
||||
assert call_args[0] == pair
|
||||
assert call_args[1] == bid
|
||||
assert call_args[2] == stake_amount / bid
|
||||
|
||||
# Test calling with price
|
||||
fix_price = 0.06
|
||||
assert freqtrade.execute_buy(pair, stake_amount, fix_price)
|
||||
# Make sure get_target_bid wasn't called again
|
||||
assert get_bid.call_count == 1
|
||||
|
||||
assert buy_mm.call_count == 2
|
||||
call_args = buy_mm.call_args_list[1][0]
|
||||
assert call_args[0] == pair
|
||||
assert call_args[1] == fix_price
|
||||
assert call_args[2] == stake_amount / fix_price
|
||||
|
||||
|
||||
def test_process_maybe_execute_buy(mocker, default_conf) -> None:
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||
|
||||
|
Reference in New Issue
Block a user