Merge branch 'develop' of https://github.com/theluxaz/freqtrade into main

# Conflicts:
#	freqtrade/freqtradebot.py
#	freqtrade/optimize/backtesting.py
This commit is contained in:
theluxaz
2021-10-13 02:01:26 +03:00
139 changed files with 3989 additions and 3843 deletions

View File

@@ -4,6 +4,7 @@ from copy import deepcopy
from fastapi import APIRouter, BackgroundTasks, Depends
from freqtrade.configuration.config_validation import validate_config_consistency
from freqtrade.enums import BacktestState
from freqtrade.exceptions import DependencyException
from freqtrade.rpc.api_server.api_schemas import BacktestRequest, BacktestResponse
@@ -42,35 +43,40 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac
# Reload strategy
lastconfig = ApiServer._bt_last_config
strat = StrategyResolver.load_strategy(btconfig)
validate_config_consistency(btconfig)
if (
not ApiServer._bt
or lastconfig.get('timeframe') != strat.timeframe
or lastconfig.get('dry_run_wallet') != btconfig.get('dry_run_wallet', 0)
or lastconfig.get('timeframe_detail') != btconfig.get('timeframe_detail')
or lastconfig.get('timerange') != btconfig['timerange']
):
from freqtrade.optimize.backtesting import Backtesting
ApiServer._bt = Backtesting(btconfig)
if ApiServer._bt.timeframe_detail:
ApiServer._bt.load_bt_data_detail()
else:
ApiServer._bt.config = btconfig
ApiServer._bt.init_backtest()
# Only reload data if timeframe changed.
if (
not ApiServer._bt_data
or not ApiServer._bt_timerange
or lastconfig.get('stake_amount') != btconfig.get('stake_amount')
or lastconfig.get('enable_protections') != btconfig.get('enable_protections')
or lastconfig.get('protections') != btconfig.get('protections', [])
or lastconfig.get('timeframe') != strat.timeframe
or lastconfig.get('timerange') != btconfig['timerange']
):
lastconfig['timerange'] = btconfig['timerange']
lastconfig['protections'] = btconfig.get('protections', [])
lastconfig['enable_protections'] = btconfig.get('enable_protections')
lastconfig['dry_run_wallet'] = btconfig.get('dry_run_wallet')
lastconfig['timeframe'] = strat.timeframe
ApiServer._bt_data, ApiServer._bt_timerange = ApiServer._bt.load_bt_data()
lastconfig['timerange'] = btconfig['timerange']
lastconfig['timeframe'] = strat.timeframe
lastconfig['protections'] = btconfig.get('protections', [])
lastconfig['enable_protections'] = btconfig.get('enable_protections')
lastconfig['dry_run_wallet'] = btconfig.get('dry_run_wallet')
ApiServer._bt.abort = False
min_date, max_date = ApiServer._bt.backtest_one_strategy(
strat, ApiServer._bt_data, ApiServer._bt_timerange)
ApiServer._bt.results = generate_backtest_stats(
ApiServer._bt_data, ApiServer._bt.all_results,
min_date=min_date, max_date=max_date)

View File

@@ -46,6 +46,12 @@ class Balances(BaseModel):
value: float
stake: str
note: str
starting_capital: float
starting_capital_ratio: float
starting_capital_pct: float
starting_capital_fiat: float
starting_capital_fiat_ratio: float
starting_capital_fiat_pct: float
class Count(BaseModel):
@@ -324,6 +330,7 @@ class PairHistory(BaseModel):
class BacktestRequest(BaseModel):
strategy: str
timeframe: Optional[str]
timeframe_detail: Optional[str]
timerange: Optional[str]
max_open_trades: Optional[int]
stake_amount: Optional[Union[float, str]]
@@ -340,3 +347,8 @@ class BacktestResponse(BaseModel):
trade_count: Optional[float]
# TODO: Properly type backtestresult...
backtest_result: Optional[Dict[str, Any]]
class SysInfo(BaseModel):
cpu_pct: List[float]
ram_pct: float

View File

@@ -18,7 +18,8 @@ from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, Blac
OpenTradeSchema, PairHistory, PerformanceEntry,
Ping, PlotConfig, Profit, ResultMsg, ShowConfig,
Stats, StatusMsg, StrategyListResponse,
StrategyResponse, Version, WhitelistResponse)
StrategyResponse, SysInfo, Version,
WhitelistResponse)
from freqtrade.rpc.api_server.deps import get_config, get_rpc, get_rpc_optional
from freqtrade.rpc.rpc import RPCException
@@ -259,3 +260,8 @@ def list_available_pairs(timeframe: Optional[str] = None, stake_currency: Option
'pair_interval': pair_interval,
}
return result
@router.get('/sysinfo', response_model=SysInfo, tags=['info'])
def sysinfo():
return RPC._rpc_sysinfo()

View File

@@ -5,6 +5,20 @@ import time
import uvicorn
def asyncio_setup() -> None: # pragma: no cover
# Set eventloop for win32 setups
# Reverts a change done in uvicorn 0.15.0 - which now sets the eventloop
# via policy.
import sys
if sys.version_info >= (3, 8) and sys.platform == "win32":
import asyncio
import selectors
selector = selectors.SelectSelector()
loop = asyncio.SelectorEventLoop(selector)
asyncio.set_event_loop(loop)
class UvicornServer(uvicorn.Server):
"""
Multithreaded server - as found in https://github.com/encode/uvicorn/issues/742
@@ -28,7 +42,7 @@ class UvicornServer(uvicorn.Server):
try:
import uvloop # noqa
except ImportError: # pragma: no cover
from uvicorn.loops.asyncio import asyncio_setup
asyncio_setup()
else:
asyncio.set_event_loop(uvloop.new_event_loop())

View File

@@ -8,6 +8,7 @@ from math import isnan
from typing import Any, Dict, List, Optional, Tuple, Union
import arrow
import psutil
from numpy import NAN, inf, int64, mean
from pandas import DataFrame
@@ -403,8 +404,11 @@ class RPC:
# Doing the sum is not right - overall profit needs to be based on initial capital
profit_all_ratio_sum = sum(profit_all_ratio) if profit_all_ratio else 0.0
starting_balance = self._freqtrade.wallets.get_starting_balance()
profit_closed_ratio_fromstart = profit_closed_coin_sum / starting_balance
profit_all_ratio_fromstart = profit_all_coin_sum / starting_balance
profit_closed_ratio_fromstart = 0
profit_all_ratio_fromstart = 0
if starting_balance:
profit_closed_ratio_fromstart = profit_closed_coin_sum / starting_balance
profit_all_ratio_fromstart = profit_all_coin_sum / starting_balance
profit_all_fiat = self._fiat_converter.convert_amount(
profit_all_coin_sum,
@@ -455,6 +459,9 @@ class RPC:
raise RPCException('Error getting current tickers.')
self._freqtrade.wallets.update(require_update=False)
starting_capital = self._freqtrade.wallets.get_starting_balance()
starting_cap_fiat = self._fiat_converter.convert_amount(
starting_capital, stake_currency, fiat_display_currency) if self._fiat_converter else 0
for coin, balance in self._freqtrade.wallets.get_all_balances().items():
if not balance.total:
@@ -490,15 +497,25 @@ class RPC:
else:
raise RPCException('All balances are zero.')
symbol = fiat_display_currency
value = self._fiat_converter.convert_amount(total, stake_currency,
symbol) if self._fiat_converter else 0
value = self._fiat_converter.convert_amount(
total, stake_currency, fiat_display_currency) if self._fiat_converter else 0
starting_capital_ratio = 0.0
starting_capital_ratio = (total / starting_capital) - 1 if starting_capital else 0.0
starting_cap_fiat_ratio = (value / starting_cap_fiat) - 1 if starting_cap_fiat else 0.0
return {
'currencies': output,
'total': total,
'symbol': symbol,
'symbol': fiat_display_currency,
'value': value,
'stake': stake_currency,
'starting_capital': starting_capital,
'starting_capital_ratio': starting_capital_ratio,
'starting_capital_pct': round(starting_capital_ratio * 100, 2),
'starting_capital_fiat': starting_cap_fiat,
'starting_capital_fiat_ratio': starting_cap_fiat_ratio,
'starting_capital_fiat_pct': round(starting_cap_fiat_ratio * 100, 2),
'note': 'Simulated balances' if self._freqtrade.config['dry_run'] else ''
}
@@ -545,12 +562,12 @@ class RPC:
order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair)
if order['side'] == 'buy':
fully_canceled = self._freqtrade.handle_cancel_buy(
fully_canceled = self._freqtrade.handle_cancel_enter(
trade, order, CANCEL_REASON['FORCE_SELL'])
if order['side'] == 'sell':
# Cancel order - so it is placed anew with a fresh price.
self._freqtrade.handle_cancel_sell(trade, order, CANCEL_REASON['FORCE_SELL'])
self._freqtrade.handle_cancel_exit(trade, order, CANCEL_REASON['FORCE_SELL'])
if not fully_canceled:
# Get current rate and execute sell
@@ -563,7 +580,7 @@ class RPC:
if self._freqtrade.state != State.RUNNING:
raise RPCException('trader is not running')
with self._freqtrade._sell_lock:
with self._freqtrade._exit_lock:
if trade_id == 'all':
# Execute sell for all open orders
for trade in Trade.get_open_trades():
@@ -625,7 +642,7 @@ class RPC:
Handler for delete <id>.
Delete the given trade and close eventually existing open orders.
"""
with self._freqtrade._sell_lock:
with self._freqtrade._exit_lock:
c_count = 0
trade = Trade.get_trades(trade_filter=[Trade.id == trade_id]).first()
if not trade:
@@ -885,3 +902,10 @@ class RPC:
'subplots' not in self._freqtrade.strategy.plot_config):
self._freqtrade.strategy.plot_config['subplots'] = {}
return self._freqtrade.strategy.plot_config
@staticmethod
def _rpc_sysinfo() -> Dict[str, Any]:
return {
"cpu_pct": psutil.cpu_percent(interval=1, percpu=True),
"ram_pct": psutil.virtual_memory().percent
}

View File

@@ -303,6 +303,50 @@ class Telegram(RPCHandler):
return message
def compose_message(self, msg: Dict[str, Any], msg_type: RPCMessageType) -> str:
if msg_type == RPCMessageType.BUY:
message = self._format_buy_msg(msg)
elif msg_type in (RPCMessageType.BUY_CANCEL, RPCMessageType.SELL_CANCEL):
msg['message_side'] = 'buy' if msg_type == RPCMessageType.BUY_CANCEL else 'sell'
message = ("\N{WARNING SIGN} *{exchange}:* "
"Cancelling open {message_side} Order for {pair} (#{trade_id}). "
"Reason: {reason}.".format(**msg))
elif msg_type == RPCMessageType.BUY_FILL:
message = ("\N{LARGE CIRCLE} *{exchange}:* "
"Buy order for {pair} (#{trade_id}) filled "
"for {open_rate}.".format(**msg))
elif msg_type == RPCMessageType.SELL_FILL:
message = ("\N{LARGE CIRCLE} *{exchange}:* "
"Sell order for {pair} (#{trade_id}) filled "
"for {close_rate}.".format(**msg))
elif msg_type == RPCMessageType.SELL:
message = self._format_sell_msg(msg)
elif msg_type == RPCMessageType.PROTECTION_TRIGGER:
message = (
"*Protection* triggered due to {reason}. "
"`{pair}` will be locked until `{lock_end_time}`."
).format(**msg)
elif msg_type == RPCMessageType.PROTECTION_TRIGGER_GLOBAL:
message = (
"*Protection* triggered due to {reason}. "
"*All pairs* will be locked until `{lock_end_time}`."
).format(**msg)
elif msg_type == RPCMessageType.STATUS:
message = '*Status:* `{status}`'.format(**msg)
elif msg_type == RPCMessageType.WARNING:
message = '\N{WARNING SIGN} *Warning:* `{status}`'.format(**msg)
elif msg_type == RPCMessageType.STARTUP:
message = '{status}'.format(**msg)
else:
raise NotImplementedError('Unknown message type: {}'.format(msg_type))
return message
def send_msg(self, msg: Dict[str, Any]) -> None:
""" Send a message to telegram channel """
@@ -327,37 +371,7 @@ class Telegram(RPCHandler):
# Notification disabled
return
if msg_type == RPCMessageType.BUY:
message = self._format_buy_msg(msg)
elif msg_type in (RPCMessageType.BUY_CANCEL, RPCMessageType.SELL_CANCEL):
msg['message_side'] = 'buy' if msg_type == RPCMessageType.BUY_CANCEL else 'sell'
message = ("\N{WARNING SIGN} *{exchange}:* "
"Cancelling open {message_side} Order for {pair} (#{trade_id}). "
"Reason: {reason}.".format(**msg))
elif msg_type == RPCMessageType.BUY_FILL:
message = ("\N{LARGE CIRCLE} *{exchange}:* "
"Buy order for {pair} (#{trade_id}) filled "
"for {open_rate}.".format(**msg))
elif msg_type == RPCMessageType.SELL_FILL:
message = ("\N{LARGE CIRCLE} *{exchange}:* "
"Sell order for {pair} (#{trade_id}) filled "
"for {close_rate}.".format(**msg))
elif msg_type == RPCMessageType.SELL:
message = self._format_sell_msg(msg)
elif msg_type == RPCMessageType.STATUS:
message = '*Status:* `{status}`'.format(**msg)
elif msg_type == RPCMessageType.WARNING:
message = '\N{WARNING SIGN} *Warning:* `{status}`'.format(**msg)
elif msg_type == RPCMessageType.STARTUP:
message = '{status}'.format(**msg)
else:
raise NotImplementedError('Unknown message type: {}'.format(msg_type))
message = self.compose_message(msg, msg_type)
self._send_msg(message, disable_notification=(noti == 'silent'))
@@ -647,12 +661,15 @@ class Telegram(RPCHandler):
output = ''
if self._config['dry_run']:
output += (
f"*Warning:* Simulated balances in Dry Mode.\n"
"This mode is still experimental!\n"
"Starting capital: "
f"`{self._config['dry_run_wallet']}` {self._config['stake_currency']}.\n"
)
output += "*Warning:* Simulated balances in Dry Mode.\n"
output += ("Starting capital: "
f"`{result['starting_capital']}` {self._config['stake_currency']}"
)
output += (f" `{result['starting_capital_fiat']}` "
f"{self._config['fiat_display_currency']}.\n"
) if result['starting_capital_fiat'] > 0 else '.\n'
total_dust_balance = 0
total_dust_currencies = 0
for curr in result['currencies']:
@@ -685,9 +702,12 @@ class Telegram(RPCHandler):
f"{round_coin_value(total_dust_balance, result['stake'], False)}`\n")
output += ("\n*Estimated Value*:\n"
f"\t`{result['stake']}: {result['total']: .8f}`\n"
f"\t`{result['stake']}: "
f"{round_coin_value(result['total'], result['stake'], False)}`"
f" `({result['starting_capital_pct']}%)`\n"
f"\t`{result['symbol']}: "
f"{round_coin_value(result['value'], result['symbol'], False)}`\n")
f"{round_coin_value(result['value'], result['symbol'], False)}`"
f" `({result['starting_capital_fiat_pct']}%)`\n")
self._send_msg(output, reload_able=True, callback_path="update_balance",
query=update.callback_query)
except RPCException as e: