merge upstream
This commit is contained in:
@@ -1,13 +1,16 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends
|
||||
|
||||
from freqtrade.configuration.config_validation import validate_config_consistency
|
||||
from freqtrade.data.btanalysis import get_backtest_resultlist, load_and_merge_backtest_result
|
||||
from freqtrade.enums import BacktestState
|
||||
from freqtrade.exceptions import DependencyException
|
||||
from freqtrade.rpc.api_server.api_schemas import BacktestRequest, BacktestResponse
|
||||
from freqtrade.rpc.api_server.api_schemas import (BacktestHistoryEntry, BacktestRequest,
|
||||
BacktestResponse)
|
||||
from freqtrade.rpc.api_server.deps import get_config, is_webserver_mode
|
||||
from freqtrade.rpc.api_server.webserver import ApiServer
|
||||
from freqtrade.rpc.rpc import RPCException
|
||||
@@ -81,6 +84,7 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac
|
||||
lastconfig['enable_protections'] = btconfig.get('enable_protections')
|
||||
lastconfig['dry_run_wallet'] = btconfig.get('dry_run_wallet')
|
||||
|
||||
ApiServer._bt.strategylist = [strat]
|
||||
ApiServer._bt.results = {}
|
||||
ApiServer._bt.load_prior_backtest()
|
||||
|
||||
@@ -200,3 +204,30 @@ def api_backtest_abort(ws_mode=Depends(is_webserver_mode)):
|
||||
"progress": 0,
|
||||
"status_msg": "Backtest ended",
|
||||
}
|
||||
|
||||
|
||||
@router.get('/backtest/history', response_model=List[BacktestHistoryEntry], tags=['webserver', 'backtest'])
|
||||
def api_backtest_history(config=Depends(get_config), ws_mode=Depends(is_webserver_mode)):
|
||||
# Get backtest result history, read from metadata files
|
||||
return get_backtest_resultlist(config['user_data_dir'] / 'backtest_results')
|
||||
|
||||
|
||||
@router.get('/backtest/history/result', response_model=BacktestResponse, tags=['webserver', 'backtest'])
|
||||
def api_backtest_history_result(filename: str, strategy: str, config=Depends(get_config), ws_mode=Depends(is_webserver_mode)):
|
||||
# Get backtest result history, read from metadata files
|
||||
fn = config['user_data_dir'] / 'backtest_results' / filename
|
||||
results: Dict[str, Any] = {
|
||||
'metadata': {},
|
||||
'strategy': {},
|
||||
'strategy_comparison': [],
|
||||
}
|
||||
|
||||
load_and_merge_backtest_result(strategy, fn, results)
|
||||
return {
|
||||
"status": "ended",
|
||||
"running": False,
|
||||
"step": "",
|
||||
"progress": 1,
|
||||
"status_msg": "Historic result",
|
||||
"backtest_result": results,
|
||||
}
|
||||
|
||||
@@ -203,6 +203,8 @@ class OrderSchema(BaseModel):
|
||||
class TradeSchema(BaseModel):
|
||||
trade_id: int
|
||||
pair: str
|
||||
base_currency: str
|
||||
quote_currency: str
|
||||
is_open: bool
|
||||
is_short: bool
|
||||
exchange: str
|
||||
@@ -289,6 +291,7 @@ class LockModel(BaseModel):
|
||||
lock_time: str
|
||||
lock_timestamp: int
|
||||
pair: str
|
||||
side: str
|
||||
reason: str
|
||||
|
||||
|
||||
@@ -419,6 +422,13 @@ class BacktestResponse(BaseModel):
|
||||
backtest_result: Optional[Dict[str, Any]]
|
||||
|
||||
|
||||
class BacktestHistoryEntry(BaseModel):
|
||||
filename: str
|
||||
strategy: str
|
||||
run_id: str
|
||||
backtest_start_time: int
|
||||
|
||||
|
||||
class SysInfo(BaseModel):
|
||||
cpu_pct: List[float]
|
||||
ram_pct: float
|
||||
|
||||
@@ -35,7 +35,8 @@ logger = logging.getLogger(__name__)
|
||||
# 1.13: forcebuy supports stake_amount
|
||||
# versions 2.xx -> futures/short branch
|
||||
# 2.14: Add entry/exit orders to trade response
|
||||
API_VERSION = 2.14
|
||||
# 2.15: Add backtest history endpoints
|
||||
API_VERSION = 2.15
|
||||
|
||||
# Public API, requires no auth.
|
||||
router_public = APIRouter()
|
||||
@@ -157,7 +158,7 @@ def force_entry(payload: ForceEnterPayload, rpc: RPC = Depends(get_rpc)):
|
||||
# /forcesell is deprecated with short addition. use /forceexit instead
|
||||
@router.post('/forceexit', response_model=ResultMsg, tags=['trading'])
|
||||
@router.post('/forcesell', response_model=ResultMsg, tags=['trading'])
|
||||
def forcesell(payload: ForceExitPayload, rpc: RPC = Depends(get_rpc)):
|
||||
def forceexit(payload: ForceExitPayload, rpc: RPC = Depends(get_rpc)):
|
||||
ordertype = payload.ordertype.value if payload.ordertype else None
|
||||
return rpc._rpc_force_exit(payload.tradeid, ordertype)
|
||||
|
||||
@@ -252,7 +253,8 @@ def list_strategies(config=Depends(get_config)):
|
||||
directory = Path(config.get(
|
||||
'strategy_path', config['user_data_dir'] / USERPATH_STRATEGIES))
|
||||
from freqtrade.resolvers.strategy_resolver import StrategyResolver
|
||||
strategies = StrategyResolver.search_all_objects(directory, False)
|
||||
strategies = StrategyResolver.search_all_objects(
|
||||
directory, False, config.get('recursive_strategy_search', False))
|
||||
strategies = sorted(strategies, key=lambda x: x['name'])
|
||||
|
||||
return {'strategies': [x['name'] for x in strategies]}
|
||||
|
||||
@@ -2,7 +2,7 @@ import logging
|
||||
from ipaddress import IPv4Address
|
||||
from typing import Any, Dict
|
||||
|
||||
import rapidjson
|
||||
import orjson
|
||||
import uvicorn
|
||||
from fastapi import Depends, FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
@@ -24,7 +24,7 @@ class FTJSONResponse(JSONResponse):
|
||||
Use rapidjson for responses
|
||||
Handles NaN and Inf / -Inf in a javascript way by default.
|
||||
"""
|
||||
return rapidjson.dumps(content).encode("utf-8")
|
||||
return orjson.dumps(content, option=orjson.OPT_SERIALIZE_NUMPY)
|
||||
|
||||
|
||||
class ApiServer(RPCHandler):
|
||||
|
||||
@@ -197,7 +197,6 @@ class RPC:
|
||||
|
||||
trade_dict = trade.to_json()
|
||||
trade_dict.update(dict(
|
||||
base_currency=self._freqtrade.config['stake_currency'],
|
||||
close_profit=trade.close_profit if not trade.is_open else None,
|
||||
current_rate=current_rate,
|
||||
current_profit=current_profit, # Deprecated
|
||||
@@ -223,6 +222,7 @@ class RPC:
|
||||
def _rpc_status_table(self, stake_currency: str,
|
||||
fiat_display_currency: str) -> Tuple[List, List, float]:
|
||||
trades: List[Trade] = Trade.get_open_trades()
|
||||
nonspot = self._config.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT
|
||||
if not trades:
|
||||
raise RPCException('no active trade')
|
||||
else:
|
||||
@@ -237,7 +237,7 @@ class RPC:
|
||||
current_rate = NAN
|
||||
trade_profit = trade.calc_profit(current_rate)
|
||||
profit_str = f'{trade.calc_profit_ratio(current_rate):.2%}'
|
||||
direction_str = 'S' if trade.is_short else 'L'
|
||||
direction_str = ('S' if trade.is_short else 'L') if nonspot else ''
|
||||
if self._fiat_converter:
|
||||
fiat_profit = self._fiat_converter.convert_amount(
|
||||
trade_profit,
|
||||
@@ -267,7 +267,11 @@ class RPC:
|
||||
if self._fiat_converter:
|
||||
profitcol += " (" + fiat_display_currency + ")"
|
||||
|
||||
columns = ['ID L/S', 'Pair', 'Since', profitcol]
|
||||
columns = [
|
||||
'ID L/S' if nonspot else 'ID',
|
||||
'Pair',
|
||||
'Since',
|
||||
profitcol]
|
||||
if self._config.get('position_adjustment_enable', False):
|
||||
columns.append('# Entries')
|
||||
return trades_list, columns, fiat_profit_sum
|
||||
@@ -686,10 +690,10 @@ class RPC:
|
||||
|
||||
def _rpc_force_exit(self, trade_id: str, ordertype: Optional[str] = None) -> Dict[str, str]:
|
||||
"""
|
||||
Handler for forcesell <id>.
|
||||
Handler for forceexit <id>.
|
||||
Sells the given trade at current price
|
||||
"""
|
||||
def _exec_forcesell(trade: Trade) -> None:
|
||||
def _exec_force_exit(trade: Trade) -> None:
|
||||
# Check if there is there is an open order
|
||||
fully_canceled = False
|
||||
if trade.open_order_id:
|
||||
@@ -722,7 +726,7 @@ class RPC:
|
||||
if trade_id == 'all':
|
||||
# Execute sell for all open orders
|
||||
for trade in Trade.get_open_trades():
|
||||
_exec_forcesell(trade)
|
||||
_exec_force_exit(trade)
|
||||
Trade.commit()
|
||||
self._freqtrade.wallets.update()
|
||||
return {'result': 'Created sell orders for all open trades.'}
|
||||
@@ -735,7 +739,7 @@ class RPC:
|
||||
logger.warning('force_exit: Invalid argument received')
|
||||
raise RPCException('invalid argument')
|
||||
|
||||
_exec_forcesell(trade)
|
||||
_exec_force_exit(trade)
|
||||
Trade.commit()
|
||||
self._freqtrade.wallets.update()
|
||||
return {'result': f'Created sell order for trade {trade_id}.'}
|
||||
|
||||
@@ -103,7 +103,6 @@ class Telegram(RPCHandler):
|
||||
['/count', '/start', '/stop', '/help']
|
||||
]
|
||||
# do not allow commands with mandatory arguments and critical cmds
|
||||
# like /forcesell and /forcebuy
|
||||
# TODO: DRY! - its not good to list all valid cmds here. But otherwise
|
||||
# this needs refactoring of the whole telegram module (same
|
||||
# problem in _help()).
|
||||
@@ -116,6 +115,7 @@ class Telegram(RPCHandler):
|
||||
r'/logs$', r'/whitelist$', r'/blacklist$', r'/bl_delete$',
|
||||
r'/weekly$', r'/weekly \d+$', r'/monthly$', r'/monthly \d+$',
|
||||
r'/forcebuy$', r'/forcelong$', r'/forceshort$',
|
||||
r'/forcesell$', r'/forceexit$',
|
||||
r'/edge$', r'/health$', r'/help$', r'/version$']
|
||||
# Create keys for generation
|
||||
valid_keys_print = [k.replace('$', '') for k in valid_keys]
|
||||
@@ -197,7 +197,8 @@ class Telegram(RPCHandler):
|
||||
pattern='update_exit_reason_performance'),
|
||||
CallbackQueryHandler(self._mix_tag_performance, pattern='update_mix_tag_performance'),
|
||||
CallbackQueryHandler(self._count, pattern='update_count'),
|
||||
CallbackQueryHandler(self._force_enter_inline),
|
||||
CallbackQueryHandler(self._force_exit_inline, pattern=r"force_exit__\S+"),
|
||||
CallbackQueryHandler(self._force_enter_inline, pattern=r"\S+\/\S+"),
|
||||
]
|
||||
for handle in handles:
|
||||
self._updater.dispatcher.add_handler(handle)
|
||||
@@ -287,7 +288,7 @@ class Telegram(RPCHandler):
|
||||
else "")
|
||||
|
||||
# Check if all sell properties are available.
|
||||
# This might not be the case if the message origin is triggered by /forcesell
|
||||
# This might not be the case if the message origin is triggered by /forceexit
|
||||
if (all(prop in msg for prop in ['gain', 'fiat_currency', 'stake_currency'])
|
||||
and self._rpc._fiat_converter):
|
||||
msg['profit_fiat'] = self._rpc._fiat_converter.convert_amount(
|
||||
@@ -431,7 +432,7 @@ class Telegram(RPCHandler):
|
||||
else:
|
||||
return "\N{CROSS MARK}"
|
||||
|
||||
def _prepare_entry_details(self, filled_orders: List, base_currency: str, is_open: bool):
|
||||
def _prepare_entry_details(self, filled_orders: List, quote_currency: str, is_open: bool):
|
||||
"""
|
||||
Prepare details of trade with entry adjustment enabled
|
||||
"""
|
||||
@@ -449,7 +450,7 @@ class Telegram(RPCHandler):
|
||||
if x == 0:
|
||||
lines.append(f"*Entry #{x+1}:*")
|
||||
lines.append(
|
||||
f"*Entry Amount:* {cur_entry_amount} ({order['cost']:.8f} {base_currency})")
|
||||
f"*Entry Amount:* {cur_entry_amount} ({order['cost']:.8f} {quote_currency})")
|
||||
lines.append(f"*Average Entry Price:* {cur_entry_average}")
|
||||
else:
|
||||
sumA = 0
|
||||
@@ -464,7 +465,8 @@ class Telegram(RPCHandler):
|
||||
if prev_avg_price:
|
||||
minus_on_entry = (cur_entry_average - prev_avg_price) / prev_avg_price
|
||||
|
||||
dur_entry = cur_entry_datetime - arrow.get(filled_orders[x-1]["order_filled_date"])
|
||||
dur_entry = cur_entry_datetime - arrow.get(
|
||||
filled_orders[x - 1]["order_filled_date"])
|
||||
days = dur_entry.days
|
||||
hours, remainder = divmod(dur_entry.seconds, 3600)
|
||||
minutes, seconds = divmod(remainder, 60)
|
||||
@@ -473,7 +475,7 @@ class Telegram(RPCHandler):
|
||||
lines.append("({})".format(cur_entry_datetime
|
||||
.humanize(granularity=["day", "hour", "minute"])))
|
||||
lines.append(
|
||||
f"*Entry Amount:* {cur_entry_amount} ({order['cost']:.8f} {base_currency})")
|
||||
f"*Entry Amount:* {cur_entry_amount} ({order['cost']:.8f} {quote_currency})")
|
||||
lines.append(f"*Average Entry Price:* {cur_entry_average} "
|
||||
f"({price_to_1st_entry:.2%} from 1st entry rate)")
|
||||
lines.append(f"*Order filled at:* {order['order_filled_date']}")
|
||||
@@ -516,7 +518,7 @@ class Telegram(RPCHandler):
|
||||
"*Current Pair:* {pair}",
|
||||
"*Direction:* " + ("`Short`" if r.get('is_short') else "`Long`"),
|
||||
"*Leverage:* `{leverage}`" if r.get('leverage') else "",
|
||||
"*Amount:* `{amount} ({stake_amount} {base_currency})`",
|
||||
"*Amount:* `{amount} ({stake_amount} {quote_currency})`",
|
||||
"*Enter Tag:* `{enter_tag}`" if r['enter_tag'] else "",
|
||||
"*Exit Reason:* `{exit_reason}`" if r['exit_reason'] else "",
|
||||
]
|
||||
@@ -556,7 +558,7 @@ class Telegram(RPCHandler):
|
||||
lines.append("*Open Order:* `{open_order}`")
|
||||
|
||||
lines_detail = self._prepare_entry_details(
|
||||
r['orders'], r['base_currency'], r['is_open'])
|
||||
r['orders'], r['quote_currency'], r['is_open'])
|
||||
lines.extend(lines_detail if lines_detail else "")
|
||||
|
||||
# Filter empty lines using list-comprehension
|
||||
@@ -976,23 +978,58 @@ class Telegram(RPCHandler):
|
||||
@authorized_only
|
||||
def _force_exit(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /forcesell <id>.
|
||||
Handler for /forceexit <id>.
|
||||
Sells the given trade at current price
|
||||
:param bot: telegram bot
|
||||
:param update: message update
|
||||
:return: None
|
||||
"""
|
||||
|
||||
trade_id = context.args[0] if context.args and len(context.args) > 0 else None
|
||||
if not trade_id:
|
||||
self._send_msg("You must specify a trade-id or 'all'.")
|
||||
return
|
||||
try:
|
||||
msg = self._rpc._rpc_force_exit(trade_id)
|
||||
self._send_msg('Force_exit Result: `{result}`'.format(**msg))
|
||||
if context.args:
|
||||
trade_id = context.args[0]
|
||||
self._force_exit_action(trade_id)
|
||||
else:
|
||||
fiat_currency = self._config.get('fiat_display_currency', '')
|
||||
try:
|
||||
statlist, _, _ = self._rpc._rpc_status_table(
|
||||
self._config['stake_currency'], fiat_currency)
|
||||
except RPCException:
|
||||
self._send_msg(msg='No open trade found.')
|
||||
return
|
||||
trades = []
|
||||
for trade in statlist:
|
||||
trades.append((trade[0], f"{trade[0]} {trade[1]} {trade[2]} {trade[3]}"))
|
||||
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e))
|
||||
trade_buttons = [
|
||||
InlineKeyboardButton(text=trade[1], callback_data=f"force_exit__{trade[0]}")
|
||||
for trade in trades]
|
||||
buttons_aligned = self._layout_inline_keyboard_onecol(trade_buttons)
|
||||
|
||||
buttons_aligned.append([InlineKeyboardButton(
|
||||
text='Cancel', callback_data='force_exit__cancel')])
|
||||
self._send_msg(msg="Which trade?", keyboard=buttons_aligned)
|
||||
|
||||
def _force_exit_action(self, trade_id):
|
||||
if trade_id != 'cancel':
|
||||
try:
|
||||
self._rpc._rpc_force_exit(trade_id)
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e))
|
||||
|
||||
def _force_exit_inline(self, update: Update, _: CallbackContext) -> None:
|
||||
if update.callback_query:
|
||||
query = update.callback_query
|
||||
if query.data and '__' in query.data:
|
||||
# Input data is "force_exit__<tradid|cancel>"
|
||||
trade_id = query.data.split("__")[1].split(' ')[0]
|
||||
if trade_id == 'cancel':
|
||||
query.answer()
|
||||
query.edit_message_text(text="Force exit canceled.")
|
||||
return
|
||||
trade: Trade = Trade.get_trades(trade_filter=Trade.id == trade_id).first()
|
||||
query.answer()
|
||||
query.edit_message_text(text=f"Manually exiting Trade #{trade_id}, {trade.pair}")
|
||||
self._force_exit_action(trade_id)
|
||||
|
||||
def _force_enter_action(self, pair, price: Optional[float], order_side: SignalDirection):
|
||||
if pair != 'cancel':
|
||||
@@ -1012,8 +1049,13 @@ class Telegram(RPCHandler):
|
||||
self._force_enter_action(pair, None, order_side)
|
||||
|
||||
@staticmethod
|
||||
def _layout_inline_keyboard(buttons: List[InlineKeyboardButton],
|
||||
cols=3) -> List[List[InlineKeyboardButton]]:
|
||||
def _layout_inline_keyboard(
|
||||
buttons: List[InlineKeyboardButton], cols=3) -> List[List[InlineKeyboardButton]]:
|
||||
return [buttons[i:i + cols] for i in range(0, len(buttons), cols)]
|
||||
|
||||
@staticmethod
|
||||
def _layout_inline_keyboard_onecol(
|
||||
buttons: List[InlineKeyboardButton], cols=1) -> List[List[InlineKeyboardButton]]:
|
||||
return [buttons[i:i + cols] for i in range(0, len(buttons), cols)]
|
||||
|
||||
@authorized_only
|
||||
@@ -1421,7 +1463,6 @@ class Telegram(RPCHandler):
|
||||
"*/start:* `Starts the trader`\n"
|
||||
"*/stop:* Stops the trader\n"
|
||||
"*/stopbuy:* `Stops buying, but handles open trades gracefully` \n"
|
||||
# TODO: forceenter forceshort forcelong missing
|
||||
"*/forceexit <trade_id>|all:* `Instantly exits the given trade or all trades, "
|
||||
"regardless of profit`\n"
|
||||
"*/fe <trade_id>|all:* `Alias to /forceexit`"
|
||||
|
||||
Reference in New Issue
Block a user