Merge branch 'develop' into 'validate_whitelist'
This commit is contained in:
commit
35d65bc7d7
@ -65,35 +65,7 @@ Where `-s TestStrategy` refers to the class name within the strategy file `test_
|
|||||||
python3 ./freqtrade/main.py backtesting --export trades
|
python3 ./freqtrade/main.py backtesting --export trades
|
||||||
```
|
```
|
||||||
|
|
||||||
The exported trades can be read using the following code for manual analysis, or can be used by the plotting script `plot_dataframe.py` in the scripts folder.
|
The exported trades can be used for [further analysis](#further-backtest-result-analysis), or can be used by the plotting script `plot_dataframe.py` in the scripts folder.
|
||||||
|
|
||||||
``` python
|
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
import pandas as pd
|
|
||||||
|
|
||||||
filename=Path('user_data/backtest_data/backtest-result.json')
|
|
||||||
|
|
||||||
with filename.open() as file:
|
|
||||||
data = json.load(file)
|
|
||||||
|
|
||||||
columns = ["pair", "profit", "opents", "closets", "index", "duration",
|
|
||||||
"open_rate", "close_rate", "open_at_end", "sell_reason"]
|
|
||||||
df = pd.DataFrame(data, columns=columns)
|
|
||||||
|
|
||||||
df['opents'] = pd.to_datetime(df['opents'],
|
|
||||||
unit='s',
|
|
||||||
utc=True,
|
|
||||||
infer_datetime_format=True
|
|
||||||
)
|
|
||||||
df['closets'] = pd.to_datetime(df['closets'],
|
|
||||||
unit='s',
|
|
||||||
utc=True,
|
|
||||||
infer_datetime_format=True
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
If you have some ideas for interesting / helpful backtest data analysis, feel free to submit a PR so the community can benefit from it.
|
|
||||||
|
|
||||||
#### Exporting trades to file specifying a custom filename
|
#### Exporting trades to file specifying a custom filename
|
||||||
|
|
||||||
@ -265,6 +237,8 @@ df.groupby("pair")["sell_reason"].value_counts()
|
|||||||
|
|
||||||
This will allow you to drill deeper into your backtest results, and perform analysis which would make the regular backtest-output unreadable.
|
This will allow you to drill deeper into your backtest results, and perform analysis which would make the regular backtest-output unreadable.
|
||||||
|
|
||||||
|
If you have some ideas for interesting / helpful backtest data analysis ideas, please submit a PR so the community can benefit from it.
|
||||||
|
|
||||||
## Backtesting multiple strategies
|
## Backtesting multiple strategies
|
||||||
|
|
||||||
To backtest multiple strategies, a list of Strategies can be provided.
|
To backtest multiple strategies, a list of Strategies can be provided.
|
||||||
|
@ -20,6 +20,7 @@ Mandatory Parameters are marked as **Required**.
|
|||||||
| `ticker_interval` | [1m, 5m, 15m, 30m, 1h, 1d, ...] | The ticker interval to use (1min, 5 min, 15 min, 30 min, 1 hour or 1 day). Default is 5 minutes. [Strategy Override](#parameters-in-the-strategy).
|
| `ticker_interval` | [1m, 5m, 15m, 30m, 1h, 1d, ...] | The ticker interval to use (1min, 5 min, 15 min, 30 min, 1 hour or 1 day). Default is 5 minutes. [Strategy Override](#parameters-in-the-strategy).
|
||||||
| `fiat_display_currency` | USD | **Required.** Fiat currency used to show your profits. More information below.
|
| `fiat_display_currency` | USD | **Required.** Fiat currency used to show your profits. More information below.
|
||||||
| `dry_run` | true | **Required.** Define if the bot must be in Dry-run or production mode.
|
| `dry_run` | true | **Required.** Define if the bot must be in Dry-run or production mode.
|
||||||
|
| `dry_run_wallet` | 999.9 | Overrides the default amount of 999.9 stake currency units in the wallet used by the bot running in the Dry Run mode if you need it for any reason.
|
||||||
| `process_only_new_candles` | false | If set to true indicators are processed only once a new candle arrives. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. [Strategy Override](#parameters-in-the-strategy).
|
| `process_only_new_candles` | false | If set to true indicators are processed only once a new candle arrives. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. [Strategy Override](#parameters-in-the-strategy).
|
||||||
| `minimal_roi` | See below | Set the threshold in percent the bot will use to sell a trade. More information below. [Strategy Override](#parameters-in-the-strategy).
|
| `minimal_roi` | See below | Set the threshold in percent the bot will use to sell a trade. More information below. [Strategy Override](#parameters-in-the-strategy).
|
||||||
| `stoploss` | -0.10 | Value of the stoploss in percent used by the bot. More information below. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy).
|
| `stoploss` | -0.10 | Value of the stoploss in percent used by the bot. More information below. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy).
|
||||||
|
@ -16,6 +16,7 @@ official commands. You can ask at any moment for help with `/help`.
|
|||||||
|----------|---------|-------------|
|
|----------|---------|-------------|
|
||||||
| `/start` | | Starts the trader
|
| `/start` | | Starts the trader
|
||||||
| `/stop` | | Stops the trader
|
| `/stop` | | Stops the trader
|
||||||
|
| `/stopbuy` | | Stops the trader from opening new trades. Gracefully closes open trades according to their rules.
|
||||||
| `/reload_conf` | | Reloads the configuration file
|
| `/reload_conf` | | Reloads the configuration file
|
||||||
| `/status` | | Lists all open trades
|
| `/status` | | Lists all open trades
|
||||||
| `/status table` | | List all open trades in a table format
|
| `/status table` | | List all open trades in a table format
|
||||||
@ -43,7 +44,21 @@ Below, example of Telegram message you will receive for each command.
|
|||||||
> `Stopping trader ...`
|
> `Stopping trader ...`
|
||||||
> **Status:** `stopped`
|
> **Status:** `stopped`
|
||||||
|
|
||||||
## /status
|
### /stopbuy
|
||||||
|
|
||||||
|
> **status:** `Setting max_open_trades to 0. Run /reload_conf to reset.`
|
||||||
|
|
||||||
|
Prevents the bot from opening new trades by temporarily setting "max_open_trades" to 0. Open trades will be handled via their regular rules (ROI / Sell-signal, stoploss, ...).
|
||||||
|
|
||||||
|
After this, give the bot time to close off open trades (can be checked via `/status table`).
|
||||||
|
Once all positions are sold, run `/stop` to completely stop the bot.
|
||||||
|
|
||||||
|
`/reload_conf` resets "max_open_trades" to the value set in the configuration and resets this command.
|
||||||
|
|
||||||
|
!!! warning:
|
||||||
|
The stop-buy signal is ONLY active while the bot is running, and is not persisted anyway, so restarting the bot will cause this to reset.
|
||||||
|
|
||||||
|
### /status
|
||||||
|
|
||||||
For each open trade, the bot will send you the following message.
|
For each open trade, the bot will send you the following message.
|
||||||
|
|
||||||
@ -58,7 +73,7 @@ For each open trade, the bot will send you the following message.
|
|||||||
> **Current Profit:** `12.95%`
|
> **Current Profit:** `12.95%`
|
||||||
> **Open Order:** `None`
|
> **Open Order:** `None`
|
||||||
|
|
||||||
## /status table
|
### /status table
|
||||||
|
|
||||||
Return the status of all open trades in a table format.
|
Return the status of all open trades in a table format.
|
||||||
```
|
```
|
||||||
@ -68,7 +83,7 @@ Return the status of all open trades in a table format.
|
|||||||
123 CVC/BTC 1 h 12.95%
|
123 CVC/BTC 1 h 12.95%
|
||||||
```
|
```
|
||||||
|
|
||||||
## /count
|
### /count
|
||||||
|
|
||||||
Return the number of trades used and available.
|
Return the number of trades used and available.
|
||||||
```
|
```
|
||||||
@ -77,7 +92,7 @@ current max
|
|||||||
2 10
|
2 10
|
||||||
```
|
```
|
||||||
|
|
||||||
## /profit
|
### /profit
|
||||||
|
|
||||||
Return a summary of your profit/loss and performance.
|
Return a summary of your profit/loss and performance.
|
||||||
|
|
||||||
@ -94,11 +109,11 @@ Return a summary of your profit/loss and performance.
|
|||||||
> **Avg. Duration:** `2:33:45`
|
> **Avg. Duration:** `2:33:45`
|
||||||
> **Best Performing:** `PAY/BTC: 50.23%`
|
> **Best Performing:** `PAY/BTC: 50.23%`
|
||||||
|
|
||||||
## /forcesell <trade_id>
|
### /forcesell <trade_id>
|
||||||
|
|
||||||
> **BITTREX:** Selling BTC/LTC with limit `0.01650000 (profit: ~-4.07%, -0.00008168)`
|
> **BITTREX:** Selling BTC/LTC with limit `0.01650000 (profit: ~-4.07%, -0.00008168)`
|
||||||
|
|
||||||
## /forcebuy <pair>
|
### /forcebuy <pair>
|
||||||
|
|
||||||
> **BITTREX**: Buying ETH/BTC with limit `0.03400000` (`1.000000 ETH`, `225.290 USD`)
|
> **BITTREX**: Buying ETH/BTC with limit `0.03400000` (`1.000000 ETH`, `225.290 USD`)
|
||||||
|
|
||||||
@ -106,7 +121,7 @@ Note that for this to work, `forcebuy_enable` needs to be set to true.
|
|||||||
|
|
||||||
[More details](configuration.md/#understand-forcebuy_enable)
|
[More details](configuration.md/#understand-forcebuy_enable)
|
||||||
|
|
||||||
## /performance
|
### /performance
|
||||||
|
|
||||||
Return the performance of each crypto-currency the bot has sold.
|
Return the performance of each crypto-currency the bot has sold.
|
||||||
> Performance:
|
> Performance:
|
||||||
@ -117,7 +132,7 @@ Return the performance of each crypto-currency the bot has sold.
|
|||||||
> 5. `STORJ/BTC 27.24%`
|
> 5. `STORJ/BTC 27.24%`
|
||||||
> ...
|
> ...
|
||||||
|
|
||||||
## /balance
|
### /balance
|
||||||
|
|
||||||
Return the balance of all crypto-currency your have on the exchange.
|
Return the balance of all crypto-currency your have on the exchange.
|
||||||
|
|
||||||
@ -131,7 +146,7 @@ Return the balance of all crypto-currency your have on the exchange.
|
|||||||
> **Balance:** 86.64180098
|
> **Balance:** 86.64180098
|
||||||
> **Pending:** 0.0
|
> **Pending:** 0.0
|
||||||
|
|
||||||
## /daily <n>
|
### /daily <n>
|
||||||
|
|
||||||
Per default `/daily` will return the 7 last days.
|
Per default `/daily` will return the 7 last days.
|
||||||
The example below if for `/daily 3`:
|
The example below if for `/daily 3`:
|
||||||
@ -145,6 +160,6 @@ Day Profit BTC Profit USD
|
|||||||
2018-01-01 0.00269130 BTC 34.986 USD
|
2018-01-01 0.00269130 BTC 34.986 USD
|
||||||
```
|
```
|
||||||
|
|
||||||
## /version
|
### /version
|
||||||
|
|
||||||
> **Version:** `0.14.3`
|
> **Version:** `0.14.3`
|
||||||
|
@ -20,6 +20,7 @@ REQUIRED_ORDERTYPES = ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']
|
|||||||
ORDERTYPE_POSSIBILITIES = ['limit', 'market']
|
ORDERTYPE_POSSIBILITIES = ['limit', 'market']
|
||||||
ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc']
|
ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc']
|
||||||
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList']
|
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList']
|
||||||
|
DRY_RUN_WALLET = 999.9
|
||||||
|
|
||||||
TICKER_INTERVAL_MINUTES = {
|
TICKER_INTERVAL_MINUTES = {
|
||||||
'1m': 1,
|
'1m': 1,
|
||||||
@ -60,6 +61,7 @@ CONF_SCHEMA = {
|
|||||||
},
|
},
|
||||||
'fiat_display_currency': {'type': 'string', 'enum': SUPPORTED_FIAT},
|
'fiat_display_currency': {'type': 'string', 'enum': SUPPORTED_FIAT},
|
||||||
'dry_run': {'type': 'boolean'},
|
'dry_run': {'type': 'boolean'},
|
||||||
|
'dry_run_wallet': {'type': 'number'},
|
||||||
'process_only_new_candles': {'type': 'boolean'},
|
'process_only_new_candles': {'type': 'boolean'},
|
||||||
'minimal_roi': {
|
'minimal_roi': {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
|
@ -66,7 +66,7 @@ def retrier(f):
|
|||||||
|
|
||||||
class Exchange(object):
|
class Exchange(object):
|
||||||
|
|
||||||
_conf: Dict = {}
|
_config: Dict = {}
|
||||||
_params: Dict = {}
|
_params: Dict = {}
|
||||||
|
|
||||||
# Dict to specify which options each exchange implements
|
# Dict to specify which options each exchange implements
|
||||||
@ -82,7 +82,7 @@ class Exchange(object):
|
|||||||
it does basic validation whether the specified exchange and pairs are valid.
|
it does basic validation whether the specified exchange and pairs are valid.
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
self._conf.update(config)
|
self._config.update(config)
|
||||||
|
|
||||||
self._cached_ticker: Dict[str, Any] = {}
|
self._cached_ticker: Dict[str, Any] = {}
|
||||||
|
|
||||||
@ -370,7 +370,7 @@ class Exchange(object):
|
|||||||
def buy(self, pair: str, ordertype: str, amount: float,
|
def buy(self, pair: str, ordertype: str, amount: float,
|
||||||
rate: float, time_in_force) -> Dict:
|
rate: float, time_in_force) -> Dict:
|
||||||
|
|
||||||
if self._conf['dry_run']:
|
if self._config['dry_run']:
|
||||||
dry_order = self.dry_run_order(pair, ordertype, "buy", amount, rate)
|
dry_order = self.dry_run_order(pair, ordertype, "buy", amount, rate)
|
||||||
return dry_order
|
return dry_order
|
||||||
|
|
||||||
@ -383,7 +383,7 @@ class Exchange(object):
|
|||||||
def sell(self, pair: str, ordertype: str, amount: float,
|
def sell(self, pair: str, ordertype: str, amount: float,
|
||||||
rate: float, time_in_force='gtc') -> Dict:
|
rate: float, time_in_force='gtc') -> Dict:
|
||||||
|
|
||||||
if self._conf['dry_run']:
|
if self._config['dry_run']:
|
||||||
dry_order = self.dry_run_order(pair, ordertype, "sell", amount, rate)
|
dry_order = self.dry_run_order(pair, ordertype, "sell", amount, rate)
|
||||||
return dry_order
|
return dry_order
|
||||||
|
|
||||||
@ -408,7 +408,7 @@ class Exchange(object):
|
|||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
'In stoploss limit order, stop price should be more than limit price')
|
'In stoploss limit order, stop price should be more than limit price')
|
||||||
|
|
||||||
if self._conf['dry_run']:
|
if self._config['dry_run']:
|
||||||
dry_order = self.dry_run_order(
|
dry_order = self.dry_run_order(
|
||||||
pair, ordertype, "sell", amount, stop_price)
|
pair, ordertype, "sell", amount, stop_price)
|
||||||
return dry_order
|
return dry_order
|
||||||
@ -423,8 +423,8 @@ class Exchange(object):
|
|||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
def get_balance(self, currency: str) -> float:
|
def get_balance(self, currency: str) -> float:
|
||||||
if self._conf['dry_run']:
|
if self._config['dry_run']:
|
||||||
return 999.9
|
return constants.DRY_RUN_WALLET
|
||||||
|
|
||||||
# ccxt exception is already handled by get_balances
|
# ccxt exception is already handled by get_balances
|
||||||
balances = self.get_balances()
|
balances = self.get_balances()
|
||||||
@ -436,7 +436,7 @@ class Exchange(object):
|
|||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
def get_balances(self) -> dict:
|
def get_balances(self) -> dict:
|
||||||
if self._conf['dry_run']:
|
if self._config['dry_run']:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -607,7 +607,7 @@ class Exchange(object):
|
|||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
def cancel_order(self, order_id: str, pair: str) -> None:
|
def cancel_order(self, order_id: str, pair: str) -> None:
|
||||||
if self._conf['dry_run']:
|
if self._config['dry_run']:
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -623,7 +623,7 @@ class Exchange(object):
|
|||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
def get_order(self, order_id: str, pair: str) -> Dict:
|
def get_order(self, order_id: str, pair: str) -> Dict:
|
||||||
if self._conf['dry_run']:
|
if self._config['dry_run']:
|
||||||
order = self._dry_run_open_orders[order_id]
|
order = self._dry_run_open_orders[order_id]
|
||||||
return order
|
return order
|
||||||
try:
|
try:
|
||||||
@ -660,7 +660,7 @@ class Exchange(object):
|
|||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
def get_trades_for_order(self, order_id: str, pair: str, since: datetime) -> List:
|
def get_trades_for_order(self, order_id: str, pair: str, since: datetime) -> List:
|
||||||
if self._conf['dry_run']:
|
if self._config['dry_run']:
|
||||||
return []
|
return []
|
||||||
if not self.exchange_has('fetchMyTrades'):
|
if not self.exchange_has('fetchMyTrades'):
|
||||||
return []
|
return []
|
||||||
|
@ -63,7 +63,7 @@ class FreqtradeBot(object):
|
|||||||
exchange_name = self.config.get('exchange', {}).get('name', 'bittrex').title()
|
exchange_name = self.config.get('exchange', {}).get('name', 'bittrex').title()
|
||||||
self.exchange = ExchangeResolver(exchange_name, self.config).exchange
|
self.exchange = ExchangeResolver(exchange_name, self.config).exchange
|
||||||
|
|
||||||
self.wallets = Wallets(self.exchange)
|
self.wallets = Wallets(self.config, self.exchange)
|
||||||
self.dataprovider = DataProvider(self.config, self.exchange)
|
self.dataprovider = DataProvider(self.config, self.exchange)
|
||||||
|
|
||||||
# Attach Dataprovider to Strategy baseclass
|
# Attach Dataprovider to Strategy baseclass
|
||||||
@ -613,6 +613,25 @@ class FreqtradeBot(object):
|
|||||||
f"(from {order_amount} to {real_amount}) from Trades")
|
f"(from {order_amount} to {real_amount}) from Trades")
|
||||||
return real_amount
|
return real_amount
|
||||||
|
|
||||||
|
def get_sell_rate(self, pair: str, refresh: bool) -> float:
|
||||||
|
"""
|
||||||
|
Get sell rate - either using get-ticker bid or first bid based on orderbook
|
||||||
|
The orderbook portion is only used for rpc messaging, which would otherwise fail
|
||||||
|
for BitMex (has no bid/ask in get_ticker)
|
||||||
|
or remain static in any other case since it's not updating.
|
||||||
|
:return: Bid rate
|
||||||
|
"""
|
||||||
|
config_ask_strategy = self.config.get('ask_strategy', {})
|
||||||
|
if config_ask_strategy.get('use_order_book', False):
|
||||||
|
logger.debug('Using order book to get sell rate')
|
||||||
|
|
||||||
|
order_book = self.exchange.get_order_book(pair, 1)
|
||||||
|
rate = order_book['bids'][0][0]
|
||||||
|
|
||||||
|
else:
|
||||||
|
rate = self.exchange.get_ticker(pair, refresh)['bid']
|
||||||
|
return rate
|
||||||
|
|
||||||
def handle_trade(self, trade: Trade) -> bool:
|
def handle_trade(self, trade: Trade) -> bool:
|
||||||
"""
|
"""
|
||||||
Sells the current pair if the threshold is reached and updates the trade record.
|
Sells the current pair if the threshold is reached and updates the trade record.
|
||||||
@ -649,7 +668,7 @@ class FreqtradeBot(object):
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
logger.debug('checking sell')
|
logger.debug('checking sell')
|
||||||
sell_rate = self.exchange.get_ticker(trade.pair)['bid']
|
sell_rate = self.get_sell_rate(trade.pair, True)
|
||||||
if self.check_sell(trade, sell_rate, buy, sell):
|
if self.check_sell(trade, sell_rate, buy, sell):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -892,7 +911,8 @@ class FreqtradeBot(object):
|
|||||||
"""
|
"""
|
||||||
profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested
|
profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested
|
||||||
profit_trade = trade.calc_profit(rate=profit_rate)
|
profit_trade = trade.calc_profit(rate=profit_rate)
|
||||||
current_rate = self.exchange.get_ticker(trade.pair)['bid']
|
# Use cached ticker here - it was updated seconds ago.
|
||||||
|
current_rate = self.get_sell_rate(trade.pair, False)
|
||||||
profit_percent = trade.calc_profit_percent(profit_rate)
|
profit_percent = trade.calc_profit_percent(profit_rate)
|
||||||
gain = "profit" if profit_percent > 0 else "loss"
|
gain = "profit" if profit_percent > 0 else "loss"
|
||||||
|
|
||||||
|
@ -94,7 +94,7 @@ class RPC(object):
|
|||||||
order = self._freqtrade.exchange.get_order(trade.open_order_id, trade.pair)
|
order = self._freqtrade.exchange.get_order(trade.open_order_id, trade.pair)
|
||||||
# calculate profit and send message to user
|
# calculate profit and send message to user
|
||||||
try:
|
try:
|
||||||
current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid']
|
current_rate = self._freqtrade.get_sell_rate(trade.pair, False)
|
||||||
except DependencyException:
|
except DependencyException:
|
||||||
current_rate = NAN
|
current_rate = NAN
|
||||||
current_profit = trade.calc_profit_percent(current_rate)
|
current_profit = trade.calc_profit_percent(current_rate)
|
||||||
@ -125,7 +125,7 @@ class RPC(object):
|
|||||||
for trade in trades:
|
for trade in trades:
|
||||||
# calculate profit and send message to user
|
# calculate profit and send message to user
|
||||||
try:
|
try:
|
||||||
current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid']
|
current_rate = self._freqtrade.get_sell_rate(trade.pair, False)
|
||||||
except DependencyException:
|
except DependencyException:
|
||||||
current_rate = NAN
|
current_rate = NAN
|
||||||
trade_perc = (100 * trade.calc_profit_percent(current_rate))
|
trade_perc = (100 * trade.calc_profit_percent(current_rate))
|
||||||
@ -213,7 +213,7 @@ class RPC(object):
|
|||||||
else:
|
else:
|
||||||
# Get current rate
|
# Get current rate
|
||||||
try:
|
try:
|
||||||
current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid']
|
current_rate = self._freqtrade.get_sell_rate(trade.pair, False)
|
||||||
except DependencyException:
|
except DependencyException:
|
||||||
current_rate = NAN
|
current_rate = NAN
|
||||||
profit_percent = trade.calc_profit_percent(rate=current_rate)
|
profit_percent = trade.calc_profit_percent(rate=current_rate)
|
||||||
@ -280,9 +280,9 @@ class RPC(object):
|
|||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
if coin == 'USDT':
|
if coin == 'USDT':
|
||||||
rate = 1.0 / self._freqtrade.exchange.get_ticker('BTC/USDT', False)['bid']
|
rate = 1.0 / self._freqtrade.get_sell_rate('BTC/USDT', False)
|
||||||
else:
|
else:
|
||||||
rate = self._freqtrade.exchange.get_ticker(coin + '/BTC', False)['bid']
|
rate = self._freqtrade.get_sell_rate(coin + '/BTC', False)
|
||||||
except (TemporaryError, DependencyException):
|
except (TemporaryError, DependencyException):
|
||||||
continue
|
continue
|
||||||
est_btc: float = rate * balance['total']
|
est_btc: float = rate * balance['total']
|
||||||
@ -328,6 +328,16 @@ class RPC(object):
|
|||||||
self._freqtrade.state = State.RELOAD_CONF
|
self._freqtrade.state = State.RELOAD_CONF
|
||||||
return {'status': 'reloading config ...'}
|
return {'status': 'reloading config ...'}
|
||||||
|
|
||||||
|
def _rpc_stopbuy(self) -> Dict[str, str]:
|
||||||
|
"""
|
||||||
|
Handler to stop buying, but handle open trades gracefully.
|
||||||
|
"""
|
||||||
|
if self._freqtrade.state == State.RUNNING:
|
||||||
|
# Set 'max_open_trades' to 0
|
||||||
|
self._freqtrade.config['max_open_trades'] = 0
|
||||||
|
|
||||||
|
return {'status': 'No more buy will occur from now. Run /reload_conf to reset.'}
|
||||||
|
|
||||||
def _rpc_forcesell(self, trade_id) -> None:
|
def _rpc_forcesell(self, trade_id) -> None:
|
||||||
"""
|
"""
|
||||||
Handler for forcesell <id>.
|
Handler for forcesell <id>.
|
||||||
@ -356,7 +366,7 @@ class RPC(object):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Get current rate and execute sell
|
# Get current rate and execute sell
|
||||||
current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid']
|
current_rate = self._freqtrade.get_sell_rate(trade.pair, False)
|
||||||
self._freqtrade.execute_sell(trade, current_rate, SellType.FORCE_SELL)
|
self._freqtrade.execute_sell(trade, current_rate, SellType.FORCE_SELL)
|
||||||
# ---- EOF def _exec_forcesell ----
|
# ---- EOF def _exec_forcesell ----
|
||||||
|
|
||||||
|
@ -91,6 +91,7 @@ class Telegram(RPC):
|
|||||||
CommandHandler('daily', self._daily),
|
CommandHandler('daily', self._daily),
|
||||||
CommandHandler('count', self._count),
|
CommandHandler('count', self._count),
|
||||||
CommandHandler('reload_conf', self._reload_conf),
|
CommandHandler('reload_conf', self._reload_conf),
|
||||||
|
CommandHandler('stopbuy', self._stopbuy),
|
||||||
CommandHandler('whitelist', self._whitelist),
|
CommandHandler('whitelist', self._whitelist),
|
||||||
CommandHandler('help', self._help),
|
CommandHandler('help', self._help),
|
||||||
CommandHandler('version', self._version),
|
CommandHandler('version', self._version),
|
||||||
@ -362,6 +363,18 @@ class Telegram(RPC):
|
|||||||
msg = self._rpc_reload_conf()
|
msg = self._rpc_reload_conf()
|
||||||
self._send_msg('Status: `{status}`'.format(**msg), bot=bot)
|
self._send_msg('Status: `{status}`'.format(**msg), bot=bot)
|
||||||
|
|
||||||
|
@authorized_only
|
||||||
|
def _stopbuy(self, bot: Bot, update: Update) -> None:
|
||||||
|
"""
|
||||||
|
Handler for /stop_buy.
|
||||||
|
Sets max_open_trades to 0 and gracefully sells all open trades
|
||||||
|
:param bot: telegram bot
|
||||||
|
:param update: message update
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
msg = self._rpc_stopbuy()
|
||||||
|
self._send_msg('Status: `{status}`'.format(**msg), bot=bot)
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _forcesell(self, bot: Bot, update: Update) -> None:
|
def _forcesell(self, bot: Bot, update: Update) -> None:
|
||||||
"""
|
"""
|
||||||
@ -481,6 +494,7 @@ class Telegram(RPC):
|
|||||||
"*/count:* `Show number of trades running compared to allowed number of trades`" \
|
"*/count:* `Show number of trades running compared to allowed number of trades`" \
|
||||||
"\n" \
|
"\n" \
|
||||||
"*/balance:* `Show account balance per currency`\n" \
|
"*/balance:* `Show account balance per currency`\n" \
|
||||||
|
"*/stopbuy:* `Stops buying, but handles open trades gracefully` \n" \
|
||||||
"*/reload_conf:* `Reload configuration file` \n" \
|
"*/reload_conf:* `Reload configuration file` \n" \
|
||||||
"*/whitelist:* `Show current whitelist` \n" \
|
"*/whitelist:* `Show current whitelist` \n" \
|
||||||
"*/help:* `This help message`\n" \
|
"*/help:* `This help message`\n" \
|
||||||
|
@ -406,6 +406,26 @@ def test_rpc_stop(mocker, default_conf) -> None:
|
|||||||
assert freqtradebot.state == State.STOPPED
|
assert freqtradebot.state == State.STOPPED
|
||||||
|
|
||||||
|
|
||||||
|
def test_rpc_stopbuy(mocker, default_conf) -> None:
|
||||||
|
patch_coinmarketcap(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
get_ticker=MagicMock()
|
||||||
|
)
|
||||||
|
|
||||||
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
|
patch_get_signal(freqtradebot, (True, False))
|
||||||
|
rpc = RPC(freqtradebot)
|
||||||
|
freqtradebot.state = State.RUNNING
|
||||||
|
|
||||||
|
assert freqtradebot.config['max_open_trades'] != 0
|
||||||
|
result = rpc._rpc_stopbuy()
|
||||||
|
assert {'status': 'No more buy will occur from now. Run /reload_conf to reset.'} == result
|
||||||
|
assert freqtradebot.config['max_open_trades'] == 0
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None:
|
def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None:
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
|
@ -74,7 +74,7 @@ def test_init(default_conf, mocker, caplog) -> None:
|
|||||||
message_str = "rpc.telegram is listening for following commands: [['status'], ['profit'], " \
|
message_str = "rpc.telegram is listening for following commands: [['status'], ['profit'], " \
|
||||||
"['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], " \
|
"['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], " \
|
||||||
"['performance'], ['daily'], ['count'], ['reload_conf'], " \
|
"['performance'], ['daily'], ['count'], ['reload_conf'], " \
|
||||||
"['whitelist'], ['help'], ['version']]"
|
"['stopbuy'], ['whitelist'], ['help'], ['version']]"
|
||||||
|
|
||||||
assert log_has(message_str, caplog.record_tuples)
|
assert log_has(message_str, caplog.record_tuples)
|
||||||
|
|
||||||
@ -662,6 +662,26 @@ def test_stop_handle_already_stopped(default_conf, update, mocker) -> None:
|
|||||||
assert 'already stopped' in msg_mock.call_args_list[0][0][0]
|
assert 'already stopped' in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_stopbuy_handle(default_conf, update, mocker) -> None:
|
||||||
|
patch_coinmarketcap(mocker)
|
||||||
|
msg_mock = MagicMock()
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
|
_init=MagicMock(),
|
||||||
|
_send_msg=msg_mock
|
||||||
|
)
|
||||||
|
|
||||||
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
|
assert freqtradebot.config['max_open_trades'] != 0
|
||||||
|
telegram._stopbuy(bot=MagicMock(), update=update)
|
||||||
|
assert freqtradebot.config['max_open_trades'] == 0
|
||||||
|
assert msg_mock.call_count == 1
|
||||||
|
assert 'No more buy will occur from now. Run /reload_conf to reset.' \
|
||||||
|
in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_reload_conf_handle(default_conf, update, mocker) -> None:
|
def test_reload_conf_handle(default_conf, update, mocker) -> None:
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
|
@ -2962,6 +2962,31 @@ def test_order_book_ask_strategy(default_conf, limit_buy_order, limit_sell_order
|
|||||||
assert freqtrade.handle_trade(trade) is True
|
assert freqtrade.handle_trade(trade) is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_sell_rate(default_conf, mocker, ticker, order_book_l2) -> None:
|
||||||
|
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
get_order_book=order_book_l2,
|
||||||
|
get_ticker=ticker,
|
||||||
|
)
|
||||||
|
pair = "ETH/BTC"
|
||||||
|
|
||||||
|
# Test regular mode
|
||||||
|
ft = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
rate = ft.get_sell_rate(pair, True)
|
||||||
|
assert isinstance(rate, float)
|
||||||
|
assert rate == 0.00001098
|
||||||
|
|
||||||
|
# Test orderbook mode
|
||||||
|
default_conf['ask_strategy']['use_order_book'] = True
|
||||||
|
default_conf['ask_strategy']['order_book_min'] = 1
|
||||||
|
default_conf['ask_strategy']['order_book_max'] = 2
|
||||||
|
ft = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
rate = ft.get_sell_rate(pair, True)
|
||||||
|
assert isinstance(rate, float)
|
||||||
|
assert rate == 0.043936
|
||||||
|
|
||||||
|
|
||||||
def test_startup_messages(default_conf, mocker):
|
def test_startup_messages(default_conf, mocker):
|
||||||
default_conf['pairlist'] = {'method': 'VolumePairList',
|
default_conf['pairlist'] = {'method': 'VolumePairList',
|
||||||
'config': {'number_assets': 20}
|
'config': {'number_assets': 20}
|
||||||
|
@ -23,13 +23,13 @@ def test_sync_wallet_at_boot(mocker, default_conf):
|
|||||||
|
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
|
||||||
assert len(freqtrade.wallets.wallets) == 2
|
assert len(freqtrade.wallets._wallets) == 2
|
||||||
assert freqtrade.wallets.wallets['BNT'].free == 1.0
|
assert freqtrade.wallets._wallets['BNT'].free == 1.0
|
||||||
assert freqtrade.wallets.wallets['BNT'].used == 2.0
|
assert freqtrade.wallets._wallets['BNT'].used == 2.0
|
||||||
assert freqtrade.wallets.wallets['BNT'].total == 3.0
|
assert freqtrade.wallets._wallets['BNT'].total == 3.0
|
||||||
assert freqtrade.wallets.wallets['GAS'].free == 0.260739
|
assert freqtrade.wallets._wallets['GAS'].free == 0.260739
|
||||||
assert freqtrade.wallets.wallets['GAS'].used == 0.0
|
assert freqtrade.wallets._wallets['GAS'].used == 0.0
|
||||||
assert freqtrade.wallets.wallets['GAS'].total == 0.260739
|
assert freqtrade.wallets._wallets['GAS'].total == 0.260739
|
||||||
assert freqtrade.wallets.get_free('BNT') == 1.0
|
assert freqtrade.wallets.get_free('BNT') == 1.0
|
||||||
|
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@ -50,13 +50,13 @@ def test_sync_wallet_at_boot(mocker, default_conf):
|
|||||||
|
|
||||||
freqtrade.wallets.update()
|
freqtrade.wallets.update()
|
||||||
|
|
||||||
assert len(freqtrade.wallets.wallets) == 2
|
assert len(freqtrade.wallets._wallets) == 2
|
||||||
assert freqtrade.wallets.wallets['BNT'].free == 1.2
|
assert freqtrade.wallets._wallets['BNT'].free == 1.2
|
||||||
assert freqtrade.wallets.wallets['BNT'].used == 1.9
|
assert freqtrade.wallets._wallets['BNT'].used == 1.9
|
||||||
assert freqtrade.wallets.wallets['BNT'].total == 3.5
|
assert freqtrade.wallets._wallets['BNT'].total == 3.5
|
||||||
assert freqtrade.wallets.wallets['GAS'].free == 0.270739
|
assert freqtrade.wallets._wallets['GAS'].free == 0.270739
|
||||||
assert freqtrade.wallets.wallets['GAS'].used == 0.1
|
assert freqtrade.wallets._wallets['GAS'].used == 0.1
|
||||||
assert freqtrade.wallets.wallets['GAS'].total == 0.260439
|
assert freqtrade.wallets._wallets['GAS'].total == 0.260439
|
||||||
assert freqtrade.wallets.get_free('GAS') == 0.270739
|
assert freqtrade.wallets.get_free('GAS') == 0.270739
|
||||||
assert freqtrade.wallets.get_used('GAS') == 0.1
|
assert freqtrade.wallets.get_used('GAS') == 0.1
|
||||||
assert freqtrade.wallets.get_total('GAS') == 0.260439
|
assert freqtrade.wallets.get_total('GAS') == 0.260439
|
||||||
@ -81,11 +81,11 @@ def test_sync_wallet_missing_data(mocker, default_conf):
|
|||||||
|
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
|
||||||
assert len(freqtrade.wallets.wallets) == 2
|
assert len(freqtrade.wallets._wallets) == 2
|
||||||
assert freqtrade.wallets.wallets['BNT'].free == 1.0
|
assert freqtrade.wallets._wallets['BNT'].free == 1.0
|
||||||
assert freqtrade.wallets.wallets['BNT'].used == 2.0
|
assert freqtrade.wallets._wallets['BNT'].used == 2.0
|
||||||
assert freqtrade.wallets.wallets['BNT'].total == 3.0
|
assert freqtrade.wallets._wallets['BNT'].total == 3.0
|
||||||
assert freqtrade.wallets.wallets['GAS'].free == 0.260739
|
assert freqtrade.wallets._wallets['GAS'].free == 0.260739
|
||||||
assert freqtrade.wallets.wallets['GAS'].used is None
|
assert freqtrade.wallets._wallets['GAS'].used is None
|
||||||
assert freqtrade.wallets.wallets['GAS'].total == 0.260739
|
assert freqtrade.wallets._wallets['GAS'].total == 0.260739
|
||||||
assert freqtrade.wallets.get_free('GAS') == 0.260739
|
assert freqtrade.wallets.get_free('GAS') == 0.260739
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
# pragma pylint: disable=W0603
|
# pragma pylint: disable=W0603
|
||||||
""" Wallet """
|
""" Wallet """
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict, Any, NamedTuple
|
from typing import Dict, NamedTuple
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
|
from freqtrade import constants
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
# wallet data structure
|
# wallet data structure
|
||||||
class Wallet(NamedTuple):
|
class Wallet(NamedTuple):
|
||||||
exchange: str
|
|
||||||
currency: str
|
currency: str
|
||||||
free: float = 0
|
free: float = 0
|
||||||
used: float = 0
|
used: float = 0
|
||||||
@ -18,17 +19,19 @@ class Wallet(NamedTuple):
|
|||||||
|
|
||||||
class Wallets(object):
|
class Wallets(object):
|
||||||
|
|
||||||
def __init__(self, exchange: Exchange) -> None:
|
def __init__(self, config: dict, exchange: Exchange) -> None:
|
||||||
self.exchange = exchange
|
self._config = config
|
||||||
self.wallets: Dict[str, Any] = {}
|
self._exchange = exchange
|
||||||
|
self._wallets: Dict[str, Wallet] = {}
|
||||||
|
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def get_free(self, currency) -> float:
|
def get_free(self, currency) -> float:
|
||||||
|
|
||||||
if self.exchange._conf['dry_run']:
|
if self._config['dry_run']:
|
||||||
return 999.9
|
return self._config.get('dry_run_wallet', constants.DRY_RUN_WALLET)
|
||||||
|
|
||||||
balance = self.wallets.get(currency)
|
balance = self._wallets.get(currency)
|
||||||
if balance and balance.free:
|
if balance and balance.free:
|
||||||
return balance.free
|
return balance.free
|
||||||
else:
|
else:
|
||||||
@ -36,10 +39,10 @@ class Wallets(object):
|
|||||||
|
|
||||||
def get_used(self, currency) -> float:
|
def get_used(self, currency) -> float:
|
||||||
|
|
||||||
if self.exchange._conf['dry_run']:
|
if self._config['dry_run']:
|
||||||
return 999.9
|
return self._config.get('dry_run_wallet', constants.DRY_RUN_WALLET)
|
||||||
|
|
||||||
balance = self.wallets.get(currency)
|
balance = self._wallets.get(currency)
|
||||||
if balance and balance.used:
|
if balance and balance.used:
|
||||||
return balance.used
|
return balance.used
|
||||||
else:
|
else:
|
||||||
@ -47,25 +50,25 @@ class Wallets(object):
|
|||||||
|
|
||||||
def get_total(self, currency) -> float:
|
def get_total(self, currency) -> float:
|
||||||
|
|
||||||
if self.exchange._conf['dry_run']:
|
if self._config['dry_run']:
|
||||||
return 999.9
|
return self._config.get('dry_run_wallet', constants.DRY_RUN_WALLET)
|
||||||
|
|
||||||
balance = self.wallets.get(currency)
|
balance = self._wallets.get(currency)
|
||||||
if balance and balance.total:
|
if balance and balance.total:
|
||||||
return balance.total
|
return balance.total
|
||||||
else:
|
else:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def update(self) -> None:
|
def update(self) -> None:
|
||||||
balances = self.exchange.get_balances()
|
|
||||||
|
balances = self._exchange.get_balances()
|
||||||
|
|
||||||
for currency in balances:
|
for currency in balances:
|
||||||
self.wallets[currency] = Wallet(
|
self._wallets[currency] = Wallet(
|
||||||
self.exchange.id,
|
|
||||||
currency,
|
currency,
|
||||||
balances[currency].get('free', None),
|
balances[currency].get('free', None),
|
||||||
balances[currency].get('used', None),
|
balances[currency].get('used', None),
|
||||||
balances[currency].get('total', None)
|
balances[currency].get('total', None)
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info('Wallets synced ...')
|
logger.info('Wallets synced.')
|
||||||
|
@ -8,5 +8,5 @@ pytest==4.3.1
|
|||||||
pytest-mock==1.10.1
|
pytest-mock==1.10.1
|
||||||
pytest-asyncio==0.10.0
|
pytest-asyncio==0.10.0
|
||||||
pytest-cov==2.6.1
|
pytest-cov==2.6.1
|
||||||
coveralls==1.6.0
|
coveralls==1.7.0
|
||||||
mypy==0.670
|
mypy==0.670
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# Include all requirements to run the bot.
|
# Include all requirements to run the bot.
|
||||||
-r requirements.txt
|
-r requirements.txt
|
||||||
|
|
||||||
plotly==3.7.0
|
plotly==3.7.1
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
ccxt==1.18.368
|
ccxt==1.18.385
|
||||||
SQLAlchemy==1.3.1
|
SQLAlchemy==1.3.1
|
||||||
python-telegram-bot==11.1.0
|
python-telegram-bot==11.1.0
|
||||||
arrow==0.13.1
|
arrow==0.13.1
|
||||||
|
Loading…
Reference in New Issue
Block a user