Merge branch 'develop' into money_mgt

This commit is contained in:
misagh 2018-11-04 18:49:21 +01:00
commit 714ac6dd08
14 changed files with 293 additions and 8 deletions

View File

@ -74,6 +74,7 @@
"chat_id": "your_telegram_chat_id" "chat_id": "your_telegram_chat_id"
}, },
"initial_state": "running", "initial_state": "running",
"forcebuy_enable": false,
"internals": { "internals": {
"process_throttle_secs": 5 "process_throttle_secs": 5
} }

View File

@ -86,6 +86,7 @@
}, },
"db_url": "sqlite:///tradesv3.sqlite", "db_url": "sqlite:///tradesv3.sqlite",
"initial_state": "running", "initial_state": "running",
"forcebuy_enable": false,
"internals": { "internals": {
"process_throttle_secs": 5 "process_throttle_secs": 5
}, },

View File

@ -53,13 +53,14 @@ The table below will list all configuration parameters.
| `telegram.enabled` | true | Yes | Enable or not the usage of Telegram. | `telegram.enabled` | true | Yes | Enable or not the usage of Telegram.
| `telegram.token` | token | No | Your Telegram bot token. Only required if `telegram.enabled` is `true`. | `telegram.token` | token | No | Your Telegram bot token. Only required if `telegram.enabled` is `true`.
| `telegram.chat_id` | chat_id | No | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. | `telegram.chat_id` | chat_id | No | Your personal Telegram account id. Only required if `telegram.enabled` is `true`.
| `webhook.enabled` | false | No | Enable useage of Webhook notifications | `webhook.enabled` | false | No | Enable usage of Webhook notifications
| `webhook.url` | false | No | URL for the webhook. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. | `webhook.url` | false | No | URL for the webhook. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details.
| `webhook.webhookbuy` | false | No | Payload to send on buy. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details. | `webhook.webhookbuy` | false | No | Payload to send on buy. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details.
| `webhook.webhooksell` | false | No | Payload to send on sell. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details. | `webhook.webhooksell` | false | No | Payload to send on sell. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details.
| `webhook.webhookstatus` | false | No | Payload to send on status calls. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details. | `webhook.webhookstatus` | false | No | Payload to send on status calls. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details.
| `db_url` | `sqlite:///tradesv3.sqlite` | No | Declares database URL to use. NOTE: This defaults to `sqlite://` if `dry_run` is `True`. | `db_url` | `sqlite:///tradesv3.sqlite` | No | Declares database URL to use. NOTE: This defaults to `sqlite://` if `dry_run` is `True`.
| `initial_state` | running | No | Defines the initial application state. More information below. | `initial_state` | running | No | Defines the initial application state. More information below.
| `forcebuy_enable` | false | No | Enables the RPC Commands to force a buy. More information below.
| `strategy` | DefaultStrategy | No | Defines Strategy class to use. | `strategy` | DefaultStrategy | No | Defines Strategy class to use.
| `strategy_path` | null | No | Adds an additional strategy lookup path (must be a folder). | `strategy_path` | null | No | Adds an additional strategy lookup path (must be a folder).
| `internals.process_throttle_secs` | 5 | Yes | Set the process throttle. Value in second. | `internals.process_throttle_secs` | 5 | Yes | Set the process throttle. Value in second.
@ -113,6 +114,15 @@ Go to the [trailing stoploss Documentation](stoploss.md) for details on trailing
Possible values are `running` or `stopped`. (default=`running`) Possible values are `running` or `stopped`. (default=`running`)
If the value is `stopped` the bot has to be started with `/start` first. If the value is `stopped` the bot has to be started with `/start` first.
### Understand forcebuy_enable
`forcebuy_enable` enables the usage of forcebuy commands via Telegram.
This is disabled for security reasons by default, and will show a warning message on startup if enabled.
You send `/forcebuy ETH/BTC` to the bot, who buys the pair and holds it until a regular sell-signal appears (ROI, stoploss, /forcesell).
Can be dangerous with some strategies, so use with care
See [the telegram documentation](telegram-usage.md) for details on usage.
### Understand process_throttle_secs ### Understand process_throttle_secs
`process_throttle_secs` is an optional field that defines in seconds how long the bot should wait `process_throttle_secs` is an optional field that defines in seconds how long the bot should wait

View File

@ -23,6 +23,7 @@ official commands. You can ask at any moment for help with `/help`.
| `/profit` | | Display a summary of your profit/loss from close trades and some stats about your performance | `/profit` | | Display a summary of your profit/loss from close trades and some stats about your performance
| `/forcesell <trade_id>` | | Instantly sells the given trade (Ignoring `minimum_roi`). | `/forcesell <trade_id>` | | Instantly sells the given trade (Ignoring `minimum_roi`).
| `/forcesell all` | | Instantly sells all open trades (Ignoring `minimum_roi`). | `/forcesell all` | | Instantly sells all open trades (Ignoring `minimum_roi`).
| `/forcebuy <pair> [rate]` | | Instantly buys the given pair. Rate is optional. (`forcebuy_enable` must be set to True)
| `/performance` | | Show performance of each finished trade grouped by pair | `/performance` | | Show performance of each finished trade grouped by pair
| `/balance` | | Show account balance per currency | `/balance` | | Show account balance per currency
| `/daily <n>` | 7 | Shows profit or loss per day, over the last n days | `/daily <n>` | 7 | Shows profit or loss per day, over the last n days
@ -30,16 +31,20 @@ official commands. You can ask at any moment for help with `/help`.
| `/version` | | Show version | `/version` | | Show version
## Telegram commands in action ## Telegram commands in action
Below, example of Telegram message you will receive for each command. Below, example of Telegram message you will receive for each command.
### /start ### /start
> **Status:** `running` > **Status:** `running`
### /stop ### /stop
> `Stopping trader ...` > `Stopping trader ...`
> **Status:** `stopped` > **Status:** `stopped`
## /status ## /status
For each open trade, the bot will send you the following message. For each open trade, the bot will send you the following message.
> **Trade ID:** `123` > **Trade ID:** `123`
@ -54,6 +59,7 @@ For each open trade, the bot will send you the following message.
> **Open Order:** `None` > **Open Order:** `None`
## /status table ## /status table
Return the status of all open trades in a table format. Return the status of all open trades in a table format.
``` ```
ID Pair Since Profit ID Pair Since Profit
@ -63,6 +69,7 @@ Return the status of all open trades in a table format.
``` ```
## /count ## /count
Return the number of trades used and available. Return the number of trades used and available.
``` ```
current max current max
@ -71,6 +78,7 @@ current max
``` ```
## /profit ## /profit
Return a summary of your profit/loss and performance. Return a summary of your profit/loss and performance.
> **ROI:** Close trades > **ROI:** Close trades
@ -90,7 +98,14 @@ Return a summary of your profit/loss and performance.
> **BITTREX:** Selling BTC/LTC with limit `0.01650000 (profit: ~-4.07%, -0.00008168)` > **BITTREX:** Selling BTC/LTC with limit `0.01650000 (profit: ~-4.07%, -0.00008168)`
## /forcebuy <pair>
> **BITTREX**: Buying ETH/BTC with limit `0.03400000` (`1.000000 ETH`, `225.290 USD`)
Note that for this to work, `forcebuy_enable` needs to be set to true.
## /performance ## /performance
Return the performance of each crypto-currency the bot has sold. Return the performance of each crypto-currency the bot has sold.
> Performance: > Performance:
> 1. `RCN/BTC 57.77%` > 1. `RCN/BTC 57.77%`
@ -101,6 +116,7 @@ Return the performance of each crypto-currency the bot has sold.
> ... > ...
## /balance ## /balance
Return the balance of all crypto-currency your have on the exchange. Return the balance of all crypto-currency your have on the exchange.
> **Currency:** BTC > **Currency:** BTC
@ -114,6 +130,7 @@ Return the balance of all crypto-currency your have on the exchange.
> **Pending:** 0.0 > **Pending:** 0.0
## /daily <n> ## /daily <n>
Per default `/daily` will return the 7 last days. Per default `/daily` will return the 7 last days.
The example below if for `/daily 3`: The example below if for `/daily 3`:
@ -127,5 +144,6 @@ Day Profit BTC Profit USD
``` ```
## /version ## /version
> **Version:** `0.14.3` > **Version:** `0.14.3`

View File

@ -127,6 +127,9 @@ class Configuration(object):
config['db_url'] = constants.DEFAULT_DB_PROD_URL config['db_url'] = constants.DEFAULT_DB_PROD_URL
logger.info('Dry run is disabled') 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"]}"') logger.info(f'Using DB: "{config["db_url"]}"')
# Check if the exchange set by the user is supported # Check if the exchange set by the user is supported

View File

@ -131,6 +131,7 @@ CONF_SCHEMA = {
}, },
'db_url': {'type': 'string'}, 'db_url': {'type': 'string'},
'initial_state': {'type': 'string', 'enum': ['running', 'stopped']}, 'initial_state': {'type': 'string', 'enum': ['running', 'stopped']},
'forcebuy_enable': {'type': 'boolean'},
'internals': { 'internals': {
'type': 'object', 'type': 'object',
'properties': { 'properties': {

View File

@ -451,7 +451,7 @@ class FreqtradeBot(object):
return True return True
return False 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 Executes a limit buy for the given pair
:param pair: pair for which we want to create a LIMIT_BUY :param pair: pair for which we want to create a LIMIT_BUY
@ -462,6 +462,9 @@ class FreqtradeBot(object):
stake_currency = self.config['stake_currency'] stake_currency = self.config['stake_currency']
fiat_currency = self.config.get('fiat_display_currency', None) fiat_currency = self.config.get('fiat_display_currency', None)
if price:
buy_limit = price
else:
# Calculate amount # Calculate amount
buy_limit = self.get_target_bid(pair, self.exchange.get_ticker(pair)) buy_limit = self.get_target_bid(pair, self.exchange.get_ticker(pair))

View File

@ -385,6 +385,40 @@ class RPC(object):
_exec_forcesell(trade) _exec_forcesell(trade)
Trade.session.flush() 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]: def _rpc_performance(self) -> List[Dict]:
""" """
Handler for performance. Handler for performance.

View File

@ -86,6 +86,7 @@ class Telegram(RPC):
CommandHandler('start', self._start), CommandHandler('start', self._start),
CommandHandler('stop', self._stop), CommandHandler('stop', self._stop),
CommandHandler('forcesell', self._forcesell), CommandHandler('forcesell', self._forcesell),
CommandHandler('forcebuy', self._forcebuy),
CommandHandler('performance', self._performance), CommandHandler('performance', self._performance),
CommandHandler('daily', self._daily), CommandHandler('daily', self._daily),
CommandHandler('count', self._count), CommandHandler('count', self._count),
@ -375,6 +376,24 @@ class Telegram(RPC):
except RPCException as e: except RPCException as e:
self._send_msg(str(e), bot=bot) 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 @authorized_only
def _performance(self, bot: Bot, update: Update) -> None: def _performance(self, bot: Bot, update: Update) -> None:
""" """

View File

@ -569,3 +569,79 @@ def test_rpc_count(mocker, default_conf, ticker, fee, markets) -> None:
trades = rpc._rpc_count() trades = rpc._rpc_count()
nb_trades = len(trades) nb_trades = len(trades)
assert nb_trades == 1 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)

View File

@ -71,8 +71,8 @@ def test_init(default_conf, mocker, caplog) -> None:
assert start_polling.start_polling.call_count == 1 assert start_polling.start_polling.call_count == 1
message_str = "rpc.telegram is listening for following commands: [['status'], ['profit'], " \ message_str = "rpc.telegram is listening for following commands: [['status'], ['profit'], " \
"['balance'], ['start'], ['stop'], ['forcesell'], ['performance'], ['daily'], " \ "['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], " \
"['count'], ['reload_conf'], ['help'], ['version']]" "['performance'], ['daily'], ['count'], ['reload_conf'], ['help'], ['version']]"
assert log_has(message_str, caplog.record_tuples) 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] 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, def test_performance_handle(default_conf, update, ticker, fee,
limit_buy_order, limit_sell_order, markets, mocker) -> None: limit_buy_order, limit_sell_order, markets, mocker) -> None:
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)

View File

@ -455,5 +455,19 @@ def test_set_loggers() -> None:
assert logging.getLogger('telegram').level is logging.INFO 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: def test_validate_default_conf(default_conf) -> None:
validate(default_conf, constants.CONF_SCHEMA) validate(default_conf, constants.CONF_SCHEMA)

View File

@ -831,6 +831,54 @@ def test_balance_bigger_last_ask(mocker, default_conf) -> None:
assert freqtrade.get_target_bid('ETH/BTC', {'ask': 5, 'last': 10}) == 5 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: def test_process_maybe_execute_buy(mocker, default_conf) -> None:
freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)

View File

@ -1,10 +1,10 @@
ccxt==1.17.455 ccxt==1.17.464
SQLAlchemy==1.2.13 SQLAlchemy==1.2.13
python-telegram-bot==11.1.0 python-telegram-bot==11.1.0
arrow==0.12.1 arrow==0.12.1
cachetools==2.1.0 cachetools==2.1.0
requests==2.20.0 requests==2.20.0
urllib3==1.24 urllib3==1.24.1
wrapt==1.10.11 wrapt==1.10.11
pandas==0.23.4 pandas==0.23.4
scikit-learn==0.20.0 scikit-learn==0.20.0