From 5f64cc8e7698508c206bd3f09e5db2607eb798bd Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 12 May 2024 16:51:11 +0200 Subject: [PATCH] ruff format: rpc modules --- freqtrade/rpc/api_server/api_auth.py | 73 +- .../rpc/api_server/api_background_tasks.py | 154 ++- freqtrade/rpc/api_server/api_backtest.py | 216 +-- freqtrade/rpc/api_server/api_schemas.py | 6 +- freqtrade/rpc/api_server/api_v1.py | 267 ++-- freqtrade/rpc/api_server/api_ws.py | 29 +- freqtrade/rpc/api_server/deps.py | 14 +- freqtrade/rpc/api_server/uvicorn_threaded.py | 4 +- freqtrade/rpc/api_server/web_ui.py | 27 +- freqtrade/rpc/api_server/webserver.py | 113 +- freqtrade/rpc/api_server/webserver_bgwork.py | 13 +- freqtrade/rpc/api_server/ws/channel.py | 17 +- freqtrade/rpc/api_server/ws/message_stream.py | 1 + freqtrade/rpc/api_server/ws/serializer.py | 9 +- freqtrade/rpc/api_server/ws_schemas.py | 4 +- freqtrade/rpc/discord.py | 51 +- freqtrade/rpc/external_message_consumer.py | 91 +- freqtrade/rpc/fiat_convert.py | 55 +- freqtrade/rpc/rpc.py | 1117 +++++++++------- freqtrade/rpc/rpc_manager.py | 124 +- freqtrade/rpc/rpc_types.py | 8 +- freqtrade/rpc/telegram.py | 1189 ++++++++++------- freqtrade/rpc/webhook.py | 101 +- 23 files changed, 1994 insertions(+), 1689 deletions(-) diff --git a/freqtrade/rpc/api_server/api_auth.py b/freqtrade/rpc/api_server/api_auth.py index 257c1cc24..0e054220b 100644 --- a/freqtrade/rpc/api_server/api_auth.py +++ b/freqtrade/rpc/api_server/api_auth.py @@ -21,8 +21,9 @@ router_login = APIRouter() def verify_auth(api_config, username: str, password: str): """Verify username/password""" - return (secrets.compare_digest(username, api_config.get('username')) and - secrets.compare_digest(password, api_config.get('password'))) + return secrets.compare_digest(username, api_config.get("username")) and secrets.compare_digest( + password, api_config.get("password") + ) httpbasic = HTTPBasic(auto_error=False) @@ -38,7 +39,7 @@ def get_user_from_token(token, secret_key: str, token_type: str = "access") -> s ) try: payload = jwt.decode(token, secret_key, algorithms=[ALGORITHM]) - username: str = payload.get("identity", {}).get('u') + username: str = payload.get("identity", {}).get("u") if username is None: raise credentials_exception if payload.get("type") != token_type: @@ -55,10 +56,10 @@ def get_user_from_token(token, secret_key: str, token_type: str = "access") -> s async def validate_ws_token( ws: WebSocket, ws_token: Union[str, None] = Query(default=None, alias="token"), - api_config: Dict[str, Any] = Depends(get_api_config) + api_config: Dict[str, Any] = Depends(get_api_config), ): - secret_ws_token = api_config.get('ws_token', None) - secret_jwt_key = api_config.get('jwt_secret_key', 'super-secret') + secret_ws_token = api_config.get("ws_token", None) + secret_jwt_key = api_config.get("jwt_secret_key", "super-secret") # Check if ws_token is/in secret_ws_token if ws_token and secret_ws_token: @@ -66,10 +67,9 @@ async def validate_ws_token( if isinstance(secret_ws_token, str): is_valid_ws_token = secrets.compare_digest(secret_ws_token, ws_token) elif isinstance(secret_ws_token, list): - is_valid_ws_token = any([ - secrets.compare_digest(potential, ws_token) - for potential in secret_ws_token - ]) + is_valid_ws_token = any( + [secrets.compare_digest(potential, ws_token) for potential in secret_ws_token] + ) if is_valid_ws_token: return ws_token @@ -94,20 +94,24 @@ def create_token(data: dict, secret_key: str, token_type: str = "access") -> str expire = datetime.now(timezone.utc) + timedelta(days=30) else: raise ValueError() - to_encode.update({ - "exp": expire, - "iat": datetime.now(timezone.utc), - "type": token_type, - }) + to_encode.update( + { + "exp": expire, + "iat": datetime.now(timezone.utc), + "type": token_type, + } + ) encoded_jwt = jwt.encode(to_encode, secret_key, algorithm=ALGORITHM) return encoded_jwt -def http_basic_or_jwt_token(form_data: HTTPBasicCredentials = Depends(httpbasic), - token: str = Depends(oauth2_scheme), - api_config=Depends(get_api_config)): +def http_basic_or_jwt_token( + form_data: HTTPBasicCredentials = Depends(httpbasic), + token: str = Depends(oauth2_scheme), + api_config=Depends(get_api_config), +): if token: - return get_user_from_token(token, api_config.get('jwt_secret_key', 'super-secret')) + return get_user_from_token(token, api_config.get("jwt_secret_key", "super-secret")) elif form_data and verify_auth(api_config, form_data.username, form_data.password): return form_data.username @@ -117,15 +121,16 @@ def http_basic_or_jwt_token(form_data: HTTPBasicCredentials = Depends(httpbasic) ) -@router_login.post('/token/login', response_model=AccessAndRefreshToken) -def token_login(form_data: HTTPBasicCredentials = Depends(security), - api_config=Depends(get_api_config)): - +@router_login.post("/token/login", response_model=AccessAndRefreshToken) +def token_login( + form_data: HTTPBasicCredentials = Depends(security), api_config=Depends(get_api_config) +): if verify_auth(api_config, form_data.username, form_data.password): - token_data = {'identity': {'u': form_data.username}} - access_token = create_token(token_data, api_config.get('jwt_secret_key', 'super-secret')) - refresh_token = create_token(token_data, api_config.get('jwt_secret_key', 'super-secret'), - token_type="refresh") + token_data = {"identity": {"u": form_data.username}} + access_token = create_token(token_data, api_config.get("jwt_secret_key", "super-secret")) + refresh_token = create_token( + token_data, api_config.get("jwt_secret_key", "super-secret"), token_type="refresh" + ) return { "access_token": access_token, "refresh_token": refresh_token, @@ -137,12 +142,12 @@ def token_login(form_data: HTTPBasicCredentials = Depends(security), ) -@router_login.post('/token/refresh', response_model=AccessToken) +@router_login.post("/token/refresh", response_model=AccessToken) def token_refresh(token: str = Depends(oauth2_scheme), api_config=Depends(get_api_config)): # Refresh token - u = get_user_from_token(token, api_config.get( - 'jwt_secret_key', 'super-secret'), 'refresh') - token_data = {'identity': {'u': u}} - access_token = create_token(token_data, api_config.get('jwt_secret_key', 'super-secret'), - token_type="access") - return {'access_token': access_token} + u = get_user_from_token(token, api_config.get("jwt_secret_key", "super-secret"), "refresh") + token_data = {"identity": {"u": u}} + access_token = create_token( + token_data, api_config.get("jwt_secret_key", "super-secret"), token_type="access" + ) + return {"access_token": access_token} diff --git a/freqtrade/rpc/api_server/api_background_tasks.py b/freqtrade/rpc/api_server/api_background_tasks.py index 7acf82f2e..6df0411c8 100644 --- a/freqtrade/rpc/api_server/api_background_tasks.py +++ b/freqtrade/rpc/api_server/api_background_tasks.py @@ -27,105 +27,113 @@ logger = logging.getLogger(__name__) router = APIRouter() -@router.get('/background', response_model=List[BackgroundTaskStatus], tags=['webserver']) +@router.get("/background", response_model=List[BackgroundTaskStatus], tags=["webserver"]) def background_job_list(): - - return [{ - 'job_id': jobid, - 'job_category': job['category'], - 'status': job['status'], - 'running': job['is_running'], - 'progress': job.get('progress'), - 'error': job.get('error', None), - } for jobid, job in ApiBG.jobs.items()] + return [ + { + "job_id": jobid, + "job_category": job["category"], + "status": job["status"], + "running": job["is_running"], + "progress": job.get("progress"), + "error": job.get("error", None), + } + for jobid, job in ApiBG.jobs.items() + ] -@router.get('/background/{jobid}', response_model=BackgroundTaskStatus, tags=['webserver']) +@router.get("/background/{jobid}", response_model=BackgroundTaskStatus, tags=["webserver"]) def background_job(jobid: str): if not (job := ApiBG.jobs.get(jobid)): - raise HTTPException(status_code=404, detail='Job not found.') + raise HTTPException(status_code=404, detail="Job not found.") return { - 'job_id': jobid, - 'job_category': job['category'], - 'status': job['status'], - 'running': job['is_running'], - 'progress': job.get('progress'), - 'error': job.get('error', None), + "job_id": jobid, + "job_category": job["category"], + "status": job["status"], + "running": job["is_running"], + "progress": job.get("progress"), + "error": job.get("error", None), } -@router.get('/pairlists/available', - response_model=PairListsResponse, tags=['pairlists', 'webserver']) +@router.get( + "/pairlists/available", response_model=PairListsResponse, tags=["pairlists", "webserver"] +) def list_pairlists(config=Depends(get_config)): from freqtrade.resolvers import PairListResolver - pairlists = PairListResolver.search_all_objects( - config, False) - pairlists = sorted(pairlists, key=lambda x: x['name']) - return {'pairlists': [{ - "name": x['name'], - "is_pairlist_generator": x['class'].is_pairlist_generator, - "params": x['class'].available_parameters(), - "description": x['class'].description(), - } for x in pairlists - ]} + pairlists = PairListResolver.search_all_objects(config, False) + pairlists = sorted(pairlists, key=lambda x: x["name"]) + + return { + "pairlists": [ + { + "name": x["name"], + "is_pairlist_generator": x["class"].is_pairlist_generator, + "params": x["class"].available_parameters(), + "description": x["class"].description(), + } + for x in pairlists + ] + } def __run_pairlist(job_id: str, config_loc: Config): try: - - ApiBG.jobs[job_id]['is_running'] = True + ApiBG.jobs[job_id]["is_running"] = True from freqtrade.plugins.pairlistmanager import PairListManager + with FtNoDBContext(): exchange = get_exchange(config_loc) pairlists = PairListManager(exchange, config_loc) pairlists.refresh_pairlist() - ApiBG.jobs[job_id]['result'] = { - 'method': pairlists.name_list, - 'length': len(pairlists.whitelist), - 'whitelist': pairlists.whitelist - } - ApiBG.jobs[job_id]['status'] = 'success' + ApiBG.jobs[job_id]["result"] = { + "method": pairlists.name_list, + "length": len(pairlists.whitelist), + "whitelist": pairlists.whitelist, + } + ApiBG.jobs[job_id]["status"] = "success" except (OperationalException, Exception) as e: logger.exception(e) - ApiBG.jobs[job_id]['error'] = str(e) - ApiBG.jobs[job_id]['status'] = 'failed' + ApiBG.jobs[job_id]["error"] = str(e) + ApiBG.jobs[job_id]["status"] = "failed" finally: - ApiBG.jobs[job_id]['is_running'] = False + ApiBG.jobs[job_id]["is_running"] = False ApiBG.pairlist_running = False -@router.post('/pairlists/evaluate', response_model=BgJobStarted, tags=['pairlists', 'webserver']) -def pairlists_evaluate(payload: PairListsPayload, background_tasks: BackgroundTasks, - config=Depends(get_config)): +@router.post("/pairlists/evaluate", response_model=BgJobStarted, tags=["pairlists", "webserver"]) +def pairlists_evaluate( + payload: PairListsPayload, background_tasks: BackgroundTasks, config=Depends(get_config) +): if ApiBG.pairlist_running: - raise HTTPException(status_code=400, detail='Pairlist evaluation is already running.') + raise HTTPException(status_code=400, detail="Pairlist evaluation is already running.") config_loc = deepcopy(config) - config_loc['stake_currency'] = payload.stake_currency - config_loc['pairlists'] = payload.pairlists + config_loc["stake_currency"] = payload.stake_currency + config_loc["pairlists"] = payload.pairlists handleExchangePayload(payload, config_loc) # TODO: overwrite blacklist? make it optional and fall back to the one in config? # Outcome depends on the UI approach. - config_loc['exchange']['pair_blacklist'] = payload.blacklist + config_loc["exchange"]["pair_blacklist"] = payload.blacklist # Random job id job_id = ApiBG.get_job_id() ApiBG.jobs[job_id] = { - 'category': 'pairlist', - 'status': 'pending', - 'progress': None, - 'is_running': False, - 'result': {}, - 'error': None, + "category": "pairlist", + "status": "pending", + "progress": None, + "is_running": False, + "result": {}, + "error": None, } background_tasks.add_task(__run_pairlist, job_id, config_loc) ApiBG.pairlist_running = True return { - 'status': 'Pairlist evaluation started in background.', - 'job_id': job_id, + "status": "Pairlist evaluation started in background.", + "job_id": job_id, } @@ -135,31 +143,35 @@ def handleExchangePayload(payload: ExchangeModePayloadMixin, config_loc: Config) Updates the configuration with the payload values. """ if payload.exchange: - config_loc['exchange']['name'] = payload.exchange + config_loc["exchange"]["name"] = payload.exchange if payload.trading_mode: - config_loc['trading_mode'] = payload.trading_mode - config_loc['candle_type_def'] = CandleType.get_default( - config_loc.get('trading_mode', 'spot') or 'spot') + config_loc["trading_mode"] = payload.trading_mode + config_loc["candle_type_def"] = CandleType.get_default( + config_loc.get("trading_mode", "spot") or "spot" + ) if payload.margin_mode: - config_loc['margin_mode'] = payload.margin_mode + config_loc["margin_mode"] = payload.margin_mode -@router.get('/pairlists/evaluate/{jobid}', response_model=WhitelistEvaluateResponse, - tags=['pairlists', 'webserver']) +@router.get( + "/pairlists/evaluate/{jobid}", + response_model=WhitelistEvaluateResponse, + tags=["pairlists", "webserver"], +) def pairlists_evaluate_get(jobid: str): if not (job := ApiBG.jobs.get(jobid)): - raise HTTPException(status_code=404, detail='Job not found.') + raise HTTPException(status_code=404, detail="Job not found.") - if job['is_running']: - raise HTTPException(status_code=400, detail='Job not finished yet.') + if job["is_running"]: + raise HTTPException(status_code=400, detail="Job not finished yet.") - if error := job['error']: + if error := job["error"]: return { - 'status': 'failed', - 'error': error, + "status": "failed", + "error": error, } return { - 'status': 'success', - 'result': job['result'], + "status": "success", + "result": job["result"], } diff --git a/freqtrade/rpc/api_server/api_backtest.py b/freqtrade/rpc/api_server/api_backtest.py index ef42a7a09..42b09de0a 100644 --- a/freqtrade/rpc/api_server/api_backtest.py +++ b/freqtrade/rpc/api_server/api_backtest.py @@ -49,67 +49,67 @@ def __run_backtest_bg(btconfig: Config): asyncio.set_event_loop(asyncio.new_event_loop()) try: # Reload strategy - lastconfig = ApiBG.bt['last_config'] + lastconfig = ApiBG.bt["last_config"] strat = StrategyResolver.load_strategy(btconfig) validate_config_consistency(btconfig) if ( - not ApiBG.bt['bt'] - or lastconfig.get('timeframe') != strat.timeframe - or lastconfig.get('timeframe_detail') != btconfig.get('timeframe_detail') - or lastconfig.get('timerange') != btconfig['timerange'] + not ApiBG.bt["bt"] + or lastconfig.get("timeframe") != strat.timeframe + or lastconfig.get("timeframe_detail") != btconfig.get("timeframe_detail") + or lastconfig.get("timerange") != btconfig["timerange"] ): from freqtrade.optimize.backtesting import Backtesting - ApiBG.bt['bt'] = Backtesting(btconfig) - ApiBG.bt['bt'].load_bt_data_detail() + + ApiBG.bt["bt"] = Backtesting(btconfig) + ApiBG.bt["bt"].load_bt_data_detail() else: - ApiBG.bt['bt'].config = btconfig - ApiBG.bt['bt'].init_backtest() + ApiBG.bt["bt"].config = btconfig + ApiBG.bt["bt"].init_backtest() # Only reload data if timeframe changed. if ( - not ApiBG.bt['data'] - or not ApiBG.bt['timerange'] - or lastconfig.get('timeframe') != strat.timeframe - or lastconfig.get('timerange') != btconfig['timerange'] + not ApiBG.bt["data"] + or not ApiBG.bt["timerange"] + or lastconfig.get("timeframe") != strat.timeframe + or lastconfig.get("timerange") != btconfig["timerange"] ): - ApiBG.bt['data'], ApiBG.bt['timerange'] = ApiBG.bt[ - 'bt'].load_bt_data() + ApiBG.bt["data"], ApiBG.bt["timerange"] = ApiBG.bt["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') + 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") - ApiBG.bt['bt'].enable_protections = btconfig.get('enable_protections', False) - ApiBG.bt['bt'].strategylist = [strat] - ApiBG.bt['bt'].results = get_BacktestResultType_default() - ApiBG.bt['bt'].load_prior_backtest() + ApiBG.bt["bt"].enable_protections = btconfig.get("enable_protections", False) + ApiBG.bt["bt"].strategylist = [strat] + ApiBG.bt["bt"].results = get_BacktestResultType_default() + ApiBG.bt["bt"].load_prior_backtest() - ApiBG.bt['bt'].abort = False + ApiBG.bt["bt"].abort = False strategy_name = strat.get_strategy_name() - if (ApiBG.bt['bt'].results and - strategy_name in ApiBG.bt['bt'].results['strategy']): + if ApiBG.bt["bt"].results and strategy_name in ApiBG.bt["bt"].results["strategy"]: # When previous result hash matches - reuse that result and skip backtesting. - logger.info(f'Reusing result of previous backtest for {strategy_name}') + logger.info(f"Reusing result of previous backtest for {strategy_name}") else: - min_date, max_date = ApiBG.bt['bt'].backtest_one_strategy( - strat, ApiBG.bt['data'], ApiBG.bt['timerange']) + min_date, max_date = ApiBG.bt["bt"].backtest_one_strategy( + strat, ApiBG.bt["data"], ApiBG.bt["timerange"] + ) - ApiBG.bt['bt'].results = generate_backtest_stats( - ApiBG.bt['data'], ApiBG.bt['bt'].all_results, - min_date=min_date, max_date=max_date) + ApiBG.bt["bt"].results = generate_backtest_stats( + ApiBG.bt["data"], ApiBG.bt["bt"].all_results, min_date=min_date, max_date=max_date + ) - if btconfig.get('export', 'none') == 'trades': - combined_res = combined_dataframes_with_rel_mean(ApiBG.bt['data'], min_date, max_date) + if btconfig.get("export", "none") == "trades": + combined_res = combined_dataframes_with_rel_mean(ApiBG.bt["data"], min_date, max_date) fn = store_backtest_stats( - btconfig['exportfilename'], - ApiBG.bt['bt'].results, + btconfig["exportfilename"], + ApiBG.bt["bt"].results, datetime.now().strftime("%Y-%m-%d_%H-%M-%S"), - market_change_data=combined_res - ) - ApiBG.bt['bt'].results['metadata'][strategy_name]['filename'] = str(fn.stem) - ApiBG.bt['bt'].results['metadata'][strategy_name]['strategy'] = strategy_name + market_change_data=combined_res, + ) + ApiBG.bt["bt"].results["metadata"][strategy_name]["filename"] = str(fn.stem) + ApiBG.bt["bt"].results["metadata"][strategy_name]["strategy"] = strategy_name logger.info("Backtest finished.") @@ -118,38 +118,38 @@ def __run_backtest_bg(btconfig: Config): except (Exception, OperationalException, DependencyException) as e: logger.exception(f"Backtesting caused an error: {e}") - ApiBG.bt['bt_error'] = str(e) + ApiBG.bt["bt_error"] = str(e) finally: ApiBG.bgtask_running = False -@router.post('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest']) +@router.post("/backtest", response_model=BacktestResponse, tags=["webserver", "backtest"]) async def api_start_backtest( - bt_settings: BacktestRequest, background_tasks: BackgroundTasks, - config=Depends(get_config)): - ApiBG.bt['bt_error'] = None + bt_settings: BacktestRequest, background_tasks: BackgroundTasks, config=Depends(get_config) +): + ApiBG.bt["bt_error"] = None """Start backtesting if not done so already""" if ApiBG.bgtask_running: - raise RPCException('Bot Background task already running') + raise RPCException("Bot Background task already running") - if ':' in bt_settings.strategy: + if ":" in bt_settings.strategy: raise HTTPException(status_code=500, detail="base64 encoded strategies are not allowed.") btconfig = deepcopy(config) - remove_exchange_credentials(btconfig['exchange'], True) + remove_exchange_credentials(btconfig["exchange"], True) settings = dict(bt_settings) - if settings.get('freqai', None) is not None: - settings['freqai'] = dict(settings['freqai']) + if settings.get("freqai", None) is not None: + settings["freqai"] = dict(settings["freqai"]) # Pydantic models will contain all keys, but non-provided ones are None btconfig = deep_merge_dicts(settings, btconfig, allow_null_overrides=False) try: - btconfig['stake_amount'] = float(btconfig['stake_amount']) + btconfig["stake_amount"] = float(btconfig["stake_amount"]) except ValueError: pass # Force dry-run for backtesting - btconfig['dry_run'] = True + btconfig["dry_run"] = True # Start backtesting # Initialize backtesting object @@ -166,39 +166,41 @@ async def api_start_backtest( } -@router.get('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest']) +@router.get("/backtest", response_model=BacktestResponse, tags=["webserver", "backtest"]) def api_get_backtest(): """ Get backtesting result. Returns Result after backtesting has been ran. """ from freqtrade.persistence import LocalTrade + if ApiBG.bgtask_running: return { "status": "running", "running": True, - "step": (ApiBG.bt['bt'].progress.action if ApiBG.bt['bt'] - else str(BacktestState.STARTUP)), - "progress": ApiBG.bt['bt'].progress.progress if ApiBG.bt['bt'] else 0, + "step": ( + ApiBG.bt["bt"].progress.action if ApiBG.bt["bt"] else str(BacktestState.STARTUP) + ), + "progress": ApiBG.bt["bt"].progress.progress if ApiBG.bt["bt"] else 0, "trade_count": len(LocalTrade.trades), "status_msg": "Backtest running", } - if not ApiBG.bt['bt']: + if not ApiBG.bt["bt"]: return { "status": "not_started", "running": False, "step": "", "progress": 0, - "status_msg": "Backtest not yet executed" + "status_msg": "Backtest not yet executed", } - if ApiBG.bt['bt_error']: + if ApiBG.bt["bt_error"]: return { "status": "error", "running": False, "step": "", "progress": 0, - "status_msg": f"Backtest failed with {ApiBG.bt['bt_error']}" + "status_msg": f"Backtest failed with {ApiBG.bt['bt_error']}", } return { @@ -207,11 +209,11 @@ def api_get_backtest(): "status_msg": "Backtest ended", "step": "finished", "progress": 1, - "backtest_result": ApiBG.bt['bt'].results, + "backtest_result": ApiBG.bt["bt"].results, } -@router.delete('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest']) +@router.delete("/backtest", response_model=BacktestResponse, tags=["webserver", "backtest"]) def api_delete_backtest(): """Reset backtesting""" if ApiBG.bgtask_running: @@ -222,12 +224,12 @@ def api_delete_backtest(): "progress": 0, "status_msg": "Backtest running", } - if ApiBG.bt['bt']: - ApiBG.bt['bt'].cleanup() - del ApiBG.bt['bt'] - ApiBG.bt['bt'] = None - del ApiBG.bt['data'] - ApiBG.bt['data'] = None + if ApiBG.bt["bt"]: + ApiBG.bt["bt"].cleanup() + del ApiBG.bt["bt"] + ApiBG.bt["bt"] = None + del ApiBG.bt["data"] + ApiBG.bt["data"] = None logger.info("Backtesting reset") return { "status": "reset", @@ -238,7 +240,7 @@ def api_delete_backtest(): } -@router.get('/backtest/abort', response_model=BacktestResponse, tags=['webserver', 'backtest']) +@router.get("/backtest/abort", response_model=BacktestResponse, tags=["webserver", "backtest"]) def api_backtest_abort(): if not ApiBG.bgtask_running: return { @@ -248,7 +250,7 @@ def api_backtest_abort(): "progress": 0, "status_msg": "Backtest ended", } - ApiBG.bt['bt'].abort = True + ApiBG.bt["bt"].abort = True return { "status": "stopping", "running": False, @@ -258,24 +260,26 @@ def api_backtest_abort(): } -@router.get('/backtest/history', response_model=List[BacktestHistoryEntry], - tags=['webserver', 'backtest']) +@router.get( + "/backtest/history", response_model=List[BacktestHistoryEntry], tags=["webserver", "backtest"] +) def api_backtest_history(config=Depends(get_config)): # Get backtest result history, read from metadata files - return get_backtest_resultlist(config['user_data_dir'] / 'backtest_results') + return get_backtest_resultlist(config["user_data_dir"] / "backtest_results") -@router.get('/backtest/history/result', response_model=BacktestResponse, - tags=['webserver', 'backtest']) +@router.get( + "/backtest/history/result", response_model=BacktestResponse, tags=["webserver", "backtest"] +) def api_backtest_history_result(filename: str, strategy: str, config=Depends(get_config)): # Get backtest result history, read from metadata files - bt_results_base: Path = config['user_data_dir'] / 'backtest_results' - fn = (bt_results_base / filename).with_suffix('.json') + bt_results_base: Path = config["user_data_dir"] / "backtest_results" + fn = (bt_results_base / filename).with_suffix(".json") results: Dict[str, Any] = { - 'metadata': {}, - 'strategy': {}, - 'strategy_comparison': [], + "metadata": {}, + "strategy": {}, + "strategy_comparison": [], } if not is_file_in_dir(fn, bt_results_base): raise HTTPException(status_code=404, detail="File not found.") @@ -290,33 +294,38 @@ def api_backtest_history_result(filename: str, strategy: str, config=Depends(get } -@router.delete('/backtest/history/{file}', response_model=List[BacktestHistoryEntry], - tags=['webserver', 'backtest']) +@router.delete( + "/backtest/history/{file}", + response_model=List[BacktestHistoryEntry], + tags=["webserver", "backtest"], +) def api_delete_backtest_history_entry(file: str, config=Depends(get_config)): # Get backtest result history, read from metadata files - bt_results_base: Path = config['user_data_dir'] / 'backtest_results' - file_abs = (bt_results_base / file).with_suffix('.json') + bt_results_base: Path = config["user_data_dir"] / "backtest_results" + file_abs = (bt_results_base / file).with_suffix(".json") # Ensure file is in backtest_results directory if not is_file_in_dir(file_abs, bt_results_base): raise HTTPException(status_code=404, detail="File not found.") delete_backtest_result(file_abs) - return get_backtest_resultlist(config['user_data_dir'] / 'backtest_results') + return get_backtest_resultlist(config["user_data_dir"] / "backtest_results") -@router.patch('/backtest/history/{file}', response_model=List[BacktestHistoryEntry], - tags=['webserver', 'backtest']) -def api_update_backtest_history_entry(file: str, body: BacktestMetadataUpdate, - config=Depends(get_config)): +@router.patch( + "/backtest/history/{file}", + response_model=List[BacktestHistoryEntry], + tags=["webserver", "backtest"], +) +def api_update_backtest_history_entry( + file: str, body: BacktestMetadataUpdate, config=Depends(get_config) +): # Get backtest result history, read from metadata files - bt_results_base: Path = config['user_data_dir'] / 'backtest_results' - file_abs = (bt_results_base / file).with_suffix('.json') + bt_results_base: Path = config["user_data_dir"] / "backtest_results" + file_abs = (bt_results_base / file).with_suffix(".json") # Ensure file is in backtest_results directory if not is_file_in_dir(file_abs, bt_results_base): raise HTTPException(status_code=404, detail="File not found.") - content = { - 'notes': body.notes - } + content = {"notes": body.notes} try: update_backtest_metadata(file_abs, body.strategy, content) except ValueError as e: @@ -325,18 +334,21 @@ def api_update_backtest_history_entry(file: str, body: BacktestMetadataUpdate, return get_backtest_result(file_abs) -@router.get('/backtest/history/{file}/market_change', response_model=BacktestMarketChange, - tags=['webserver', 'backtest']) +@router.get( + "/backtest/history/{file}/market_change", + response_model=BacktestMarketChange, + tags=["webserver", "backtest"], +) def api_get_backtest_market_change(file: str, config=Depends(get_config)): - bt_results_base: Path = config['user_data_dir'] / 'backtest_results' - file_abs = (bt_results_base / f"{file}_market_change").with_suffix('.feather') + bt_results_base: Path = config["user_data_dir"] / "backtest_results" + file_abs = (bt_results_base / f"{file}_market_change").with_suffix(".feather") # Ensure file is in backtest_results directory if not is_file_in_dir(file_abs, bt_results_base): raise HTTPException(status_code=404, detail="File not found.") df = get_backtest_market_change(file_abs) return { - 'columns': df.columns.tolist(), - 'data': df.values.tolist(), - 'length': len(df), + "columns": df.columns.tolist(), + "data": df.values.tolist(), + "length": len(df), } diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index c77c1da07..0e36c0992 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -381,7 +381,7 @@ class Locks(BaseModel): class LocksPayload(BaseModel): pair: str - side: str = '*' # Default to both sides + side: str = "*" # Default to both sides until: AwareDatetime reason: Optional[str] = None @@ -561,7 +561,7 @@ class BacktestHistoryEntry(BaseModel): strategy: str run_id: str backtest_start_time: int - notes: Optional[str] = '' + notes: Optional[str] = "" backtest_start_ts: Optional[int] = None backtest_end_ts: Optional[int] = None timeframe: Optional[str] = None @@ -570,7 +570,7 @@ class BacktestHistoryEntry(BaseModel): class BacktestMetadataUpdate(BaseModel): strategy: str - notes: str = '' + notes: str = "" class BacktestMarketChange(BaseModel): diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index e46f9e9af..6774368ee 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -90,80 +90,84 @@ router_public = APIRouter() router = APIRouter() -@router_public.get('/ping', response_model=Ping) +@router_public.get("/ping", response_model=Ping) def ping(): """simple ping""" return {"status": "pong"} -@router.get('/version', response_model=Version, tags=['info']) +@router.get("/version", response_model=Version, tags=["info"]) def version(): - """ Bot Version info""" + """Bot Version info""" return {"version": __version__} -@router.get('/balance', response_model=Balances, tags=['info']) +@router.get("/balance", response_model=Balances, tags=["info"]) def balance(rpc: RPC = Depends(get_rpc), config=Depends(get_config)): """Account Balances""" - return rpc._rpc_balance(config['stake_currency'], config.get('fiat_display_currency', ''),) + return rpc._rpc_balance( + config["stake_currency"], + config.get("fiat_display_currency", ""), + ) -@router.get('/count', response_model=Count, tags=['info']) +@router.get("/count", response_model=Count, tags=["info"]) def count(rpc: RPC = Depends(get_rpc)): return rpc._rpc_count() -@router.get('/entries', response_model=List[Entry], tags=['info']) +@router.get("/entries", response_model=List[Entry], tags=["info"]) def entries(pair: Optional[str] = None, rpc: RPC = Depends(get_rpc)): return rpc._rpc_enter_tag_performance(pair) -@router.get('/exits', response_model=List[Exit], tags=['info']) +@router.get("/exits", response_model=List[Exit], tags=["info"]) def exits(pair: Optional[str] = None, rpc: RPC = Depends(get_rpc)): return rpc._rpc_exit_reason_performance(pair) -@router.get('/mix_tags', response_model=List[MixTag], tags=['info']) +@router.get("/mix_tags", response_model=List[MixTag], tags=["info"]) def mix_tags(pair: Optional[str] = None, rpc: RPC = Depends(get_rpc)): return rpc._rpc_mix_tag_performance(pair) -@router.get('/performance', response_model=List[PerformanceEntry], tags=['info']) +@router.get("/performance", response_model=List[PerformanceEntry], tags=["info"]) def performance(rpc: RPC = Depends(get_rpc)): return rpc._rpc_performance() -@router.get('/profit', response_model=Profit, tags=['info']) +@router.get("/profit", response_model=Profit, tags=["info"]) def profit(rpc: RPC = Depends(get_rpc), config=Depends(get_config)): - return rpc._rpc_trade_statistics(config['stake_currency'], - config.get('fiat_display_currency') - ) + return rpc._rpc_trade_statistics(config["stake_currency"], config.get("fiat_display_currency")) -@router.get('/stats', response_model=Stats, tags=['info']) +@router.get("/stats", response_model=Stats, tags=["info"]) def stats(rpc: RPC = Depends(get_rpc)): return rpc._rpc_stats() -@router.get('/daily', response_model=DailyWeeklyMonthly, tags=['info']) +@router.get("/daily", response_model=DailyWeeklyMonthly, tags=["info"]) def daily(timescale: int = 7, rpc: RPC = Depends(get_rpc), config=Depends(get_config)): - return rpc._rpc_timeunit_profit(timescale, config['stake_currency'], - config.get('fiat_display_currency', '')) + return rpc._rpc_timeunit_profit( + timescale, config["stake_currency"], config.get("fiat_display_currency", "") + ) -@router.get('/weekly', response_model=DailyWeeklyMonthly, tags=['info']) +@router.get("/weekly", response_model=DailyWeeklyMonthly, tags=["info"]) def weekly(timescale: int = 4, rpc: RPC = Depends(get_rpc), config=Depends(get_config)): - return rpc._rpc_timeunit_profit(timescale, config['stake_currency'], - config.get('fiat_display_currency', ''), 'weeks') + return rpc._rpc_timeunit_profit( + timescale, config["stake_currency"], config.get("fiat_display_currency", ""), "weeks" + ) -@router.get('/monthly', response_model=DailyWeeklyMonthly, tags=['info']) +@router.get("/monthly", response_model=DailyWeeklyMonthly, tags=["info"]) def monthly(timescale: int = 3, rpc: RPC = Depends(get_rpc), config=Depends(get_config)): - return rpc._rpc_timeunit_profit(timescale, config['stake_currency'], - config.get('fiat_display_currency', ''), 'months') + return rpc._rpc_timeunit_profit( + timescale, config["stake_currency"], config.get("fiat_display_currency", ""), "months" + ) -@router.get('/status', response_model=List[OpenTradeSchema], tags=['info']) +@router.get("/status", response_model=List[OpenTradeSchema], tags=["info"]) def status(rpc: RPC = Depends(get_rpc)): try: return rpc._rpc_trade_status() @@ -173,274 +177,305 @@ def status(rpc: RPC = Depends(get_rpc)): # Using the responsemodel here will cause a ~100% increase in response time (from 1s to 2s) # on big databases. Correct response model: response_model=TradeResponse, -@router.get('/trades', tags=['info', 'trading']) +@router.get("/trades", tags=["info", "trading"]) def trades(limit: int = 500, offset: int = 0, rpc: RPC = Depends(get_rpc)): return rpc._rpc_trade_history(limit, offset=offset, order_by_id=True) -@router.get('/trade/{tradeid}', response_model=OpenTradeSchema, tags=['info', 'trading']) +@router.get("/trade/{tradeid}", response_model=OpenTradeSchema, tags=["info", "trading"]) def trade(tradeid: int = 0, rpc: RPC = Depends(get_rpc)): try: return rpc._rpc_trade_status([tradeid])[0] except (RPCException, KeyError): - raise HTTPException(status_code=404, detail='Trade not found.') + raise HTTPException(status_code=404, detail="Trade not found.") -@router.delete('/trades/{tradeid}', response_model=DeleteTrade, tags=['info', 'trading']) +@router.delete("/trades/{tradeid}", response_model=DeleteTrade, tags=["info", "trading"]) def trades_delete(tradeid: int, rpc: RPC = Depends(get_rpc)): return rpc._rpc_delete(tradeid) -@router.delete('/trades/{tradeid}/open-order', response_model=OpenTradeSchema, tags=['trading']) +@router.delete("/trades/{tradeid}/open-order", response_model=OpenTradeSchema, tags=["trading"]) def trade_cancel_open_order(tradeid: int, rpc: RPC = Depends(get_rpc)): rpc._rpc_cancel_open_order(tradeid) return rpc._rpc_trade_status([tradeid])[0] -@router.post('/trades/{tradeid}/reload', response_model=OpenTradeSchema, tags=['trading']) +@router.post("/trades/{tradeid}/reload", response_model=OpenTradeSchema, tags=["trading"]) def trade_reload(tradeid: int, rpc: RPC = Depends(get_rpc)): rpc._rpc_reload_trade_from_exchange(tradeid) return rpc._rpc_trade_status([tradeid])[0] # TODO: Missing response model -@router.get('/edge', tags=['info']) +@router.get("/edge", tags=["info"]) def edge(rpc: RPC = Depends(get_rpc)): return rpc._rpc_edge() -@router.get('/show_config', response_model=ShowConfig, tags=['info']) +@router.get("/show_config", response_model=ShowConfig, tags=["info"]) def show_config(rpc: Optional[RPC] = Depends(get_rpc_optional), config=Depends(get_config)): - state = '' + state = "" strategy_version = None if rpc: state = rpc._freqtrade.state strategy_version = rpc._freqtrade.strategy.version() resp = RPC._rpc_show_config(config, state, strategy_version) - resp['api_version'] = API_VERSION + resp["api_version"] = API_VERSION return resp # /forcebuy is deprecated with short addition. use /forceentry instead -@router.post('/forceenter', response_model=ForceEnterResponse, tags=['trading']) -@router.post('/forcebuy', response_model=ForceEnterResponse, tags=['trading']) +@router.post("/forceenter", response_model=ForceEnterResponse, tags=["trading"]) +@router.post("/forcebuy", response_model=ForceEnterResponse, tags=["trading"]) def force_entry(payload: ForceEnterPayload, rpc: RPC = Depends(get_rpc)): ordertype = payload.ordertype.value if payload.ordertype else None - trade = rpc._rpc_force_entry(payload.pair, payload.price, order_side=payload.side, - order_type=ordertype, stake_amount=payload.stakeamount, - enter_tag=payload.entry_tag or 'force_entry', - leverage=payload.leverage) + trade = rpc._rpc_force_entry( + payload.pair, + payload.price, + order_side=payload.side, + order_type=ordertype, + stake_amount=payload.stakeamount, + enter_tag=payload.entry_tag or "force_entry", + leverage=payload.leverage, + ) if trade: return ForceEnterResponse.model_validate(trade.to_json()) else: return ForceEnterResponse.model_validate( - {"status": f"Error entering {payload.side} trade for pair {payload.pair}."}) + {"status": f"Error entering {payload.side} trade for pair {payload.pair}."} + ) # /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']) +@router.post("/forceexit", response_model=ResultMsg, tags=["trading"]) +@router.post("/forcesell", response_model=ResultMsg, tags=["trading"]) def forceexit(payload: ForceExitPayload, rpc: RPC = Depends(get_rpc)): ordertype = payload.ordertype.value if payload.ordertype else None return rpc._rpc_force_exit(str(payload.tradeid), ordertype, amount=payload.amount) -@router.get('/blacklist', response_model=BlacklistResponse, tags=['info', 'pairlist']) +@router.get("/blacklist", response_model=BlacklistResponse, tags=["info", "pairlist"]) def blacklist(rpc: RPC = Depends(get_rpc)): return rpc._rpc_blacklist() -@router.post('/blacklist', response_model=BlacklistResponse, tags=['info', 'pairlist']) +@router.post("/blacklist", response_model=BlacklistResponse, tags=["info", "pairlist"]) def blacklist_post(payload: BlacklistPayload, rpc: RPC = Depends(get_rpc)): return rpc._rpc_blacklist(payload.blacklist) -@router.delete('/blacklist', response_model=BlacklistResponse, tags=['info', 'pairlist']) +@router.delete("/blacklist", response_model=BlacklistResponse, tags=["info", "pairlist"]) def blacklist_delete(pairs_to_delete: List[str] = Query([]), rpc: RPC = Depends(get_rpc)): """Provide a list of pairs to delete from the blacklist""" return rpc._rpc_blacklist_delete(pairs_to_delete) -@router.get('/whitelist', response_model=WhitelistResponse, tags=['info', 'pairlist']) +@router.get("/whitelist", response_model=WhitelistResponse, tags=["info", "pairlist"]) def whitelist(rpc: RPC = Depends(get_rpc)): return rpc._rpc_whitelist() -@router.get('/locks', response_model=Locks, tags=['info', 'locks']) +@router.get("/locks", response_model=Locks, tags=["info", "locks"]) def locks(rpc: RPC = Depends(get_rpc)): return rpc._rpc_locks() -@router.delete('/locks/{lockid}', response_model=Locks, tags=['info', 'locks']) +@router.delete("/locks/{lockid}", response_model=Locks, tags=["info", "locks"]) def delete_lock(lockid: int, rpc: RPC = Depends(get_rpc)): return rpc._rpc_delete_lock(lockid=lockid) -@router.post('/locks/delete', response_model=Locks, tags=['info', 'locks']) +@router.post("/locks/delete", response_model=Locks, tags=["info", "locks"]) def delete_lock_pair(payload: DeleteLockRequest, rpc: RPC = Depends(get_rpc)): return rpc._rpc_delete_lock(lockid=payload.lockid, pair=payload.pair) -@router.post('/locks', response_model=Locks, tags=['info', 'locks']) +@router.post("/locks", response_model=Locks, tags=["info", "locks"]) def add_locks(payload: List[LocksPayload], rpc: RPC = Depends(get_rpc)): for lock in payload: rpc._rpc_add_lock(lock.pair, lock.until, lock.reason, lock.side) return rpc._rpc_locks() -@router.get('/logs', response_model=Logs, tags=['info']) +@router.get("/logs", response_model=Logs, tags=["info"]) def logs(limit: Optional[int] = None): return RPC._rpc_get_logs(limit) -@router.post('/start', response_model=StatusMsg, tags=['botcontrol']) +@router.post("/start", response_model=StatusMsg, tags=["botcontrol"]) def start(rpc: RPC = Depends(get_rpc)): return rpc._rpc_start() -@router.post('/stop', response_model=StatusMsg, tags=['botcontrol']) +@router.post("/stop", response_model=StatusMsg, tags=["botcontrol"]) def stop(rpc: RPC = Depends(get_rpc)): return rpc._rpc_stop() -@router.post('/stopentry', response_model=StatusMsg, tags=['botcontrol']) -@router.post('/stopbuy', response_model=StatusMsg, tags=['botcontrol']) +@router.post("/stopentry", response_model=StatusMsg, tags=["botcontrol"]) +@router.post("/stopbuy", response_model=StatusMsg, tags=["botcontrol"]) def stop_buy(rpc: RPC = Depends(get_rpc)): return rpc._rpc_stopentry() -@router.post('/reload_config', response_model=StatusMsg, tags=['botcontrol']) +@router.post("/reload_config", response_model=StatusMsg, tags=["botcontrol"]) def reload_config(rpc: RPC = Depends(get_rpc)): return rpc._rpc_reload_config() -@router.get('/pair_candles', response_model=PairHistory, tags=['candle data']) +@router.get("/pair_candles", response_model=PairHistory, tags=["candle data"]) def pair_candles( - pair: str, timeframe: str, limit: Optional[int] = None, rpc: RPC = Depends(get_rpc)): + pair: str, timeframe: str, limit: Optional[int] = None, rpc: RPC = Depends(get_rpc) +): return rpc._rpc_analysed_dataframe(pair, timeframe, limit, None) -@router.post('/pair_candles', response_model=PairHistory, tags=['candle data']) +@router.post("/pair_candles", response_model=PairHistory, tags=["candle data"]) def pair_candles_filtered(payload: PairCandlesRequest, rpc: RPC = Depends(get_rpc)): # Advanced pair_candles endpoint with column filtering return rpc._rpc_analysed_dataframe( - payload.pair, payload.timeframe, payload.limit, payload.columns) + payload.pair, payload.timeframe, payload.limit, payload.columns + ) -@router.get('/pair_history', response_model=PairHistory, tags=['candle data']) -def pair_history(pair: str, timeframe: str, timerange: str, strategy: str, - freqaimodel: Optional[str] = None, - config=Depends(get_config), exchange=Depends(get_exchange)): +@router.get("/pair_history", response_model=PairHistory, tags=["candle data"]) +def pair_history( + pair: str, + timeframe: str, + timerange: str, + strategy: str, + freqaimodel: Optional[str] = None, + config=Depends(get_config), + exchange=Depends(get_exchange), +): # The initial call to this endpoint can be slow, as it may need to initialize # the exchange class. config = deepcopy(config) - config.update({ - 'strategy': strategy, - 'timerange': timerange, - 'freqaimodel': freqaimodel if freqaimodel else config.get('freqaimodel'), - }) + config.update( + { + "strategy": strategy, + "timerange": timerange, + "freqaimodel": freqaimodel if freqaimodel else config.get("freqaimodel"), + } + ) try: return RPC._rpc_analysed_history_full(config, pair, timeframe, exchange, None) except Exception as e: raise HTTPException(status_code=502, detail=str(e)) -@router.post('/pair_history', response_model=PairHistory, tags=['candle data']) -def pair_history_filtered(payload: PairHistoryRequest, - config=Depends(get_config), exchange=Depends(get_exchange)): +@router.post("/pair_history", response_model=PairHistory, tags=["candle data"]) +def pair_history_filtered( + payload: PairHistoryRequest, config=Depends(get_config), exchange=Depends(get_exchange) +): # The initial call to this endpoint can be slow, as it may need to initialize # the exchange class. config = deepcopy(config) - config.update({ - 'strategy': payload.strategy, - 'timerange': payload.timerange, - 'freqaimodel': payload.freqaimodel if payload.freqaimodel else config.get('freqaimodel'), - }) + config.update( + { + "strategy": payload.strategy, + "timerange": payload.timerange, + "freqaimodel": payload.freqaimodel + if payload.freqaimodel + else config.get("freqaimodel"), + } + ) try: return RPC._rpc_analysed_history_full( - config, payload.pair, payload.timeframe, exchange, payload.columns) + config, payload.pair, payload.timeframe, exchange, payload.columns + ) except Exception as e: raise HTTPException(status_code=502, detail=str(e)) -@router.get('/plot_config', response_model=PlotConfig, tags=['candle data']) -def plot_config(strategy: Optional[str] = None, config=Depends(get_config), - rpc: Optional[RPC] = Depends(get_rpc_optional)): +@router.get("/plot_config", response_model=PlotConfig, tags=["candle data"]) +def plot_config( + strategy: Optional[str] = None, + config=Depends(get_config), + rpc: Optional[RPC] = Depends(get_rpc_optional), +): if not strategy: if not rpc: raise RPCException("Strategy is mandatory in webserver mode.") return PlotConfig.model_validate(rpc._rpc_plot_config()) else: config1 = deepcopy(config) - config1.update({ - 'strategy': strategy - }) + config1.update({"strategy": strategy}) try: return PlotConfig.model_validate(RPC._rpc_plot_config_with_strategy(config1)) except Exception as e: raise HTTPException(status_code=502, detail=str(e)) -@router.get('/strategies', response_model=StrategyListResponse, tags=['strategy']) +@router.get("/strategies", response_model=StrategyListResponse, tags=["strategy"]) def list_strategies(config=Depends(get_config)): from freqtrade.resolvers.strategy_resolver import StrategyResolver + strategies = StrategyResolver.search_all_objects( - config, False, config.get('recursive_strategy_search', False)) - strategies = sorted(strategies, key=lambda x: x['name']) + config, False, config.get("recursive_strategy_search", False) + ) + strategies = sorted(strategies, key=lambda x: x["name"]) - return {'strategies': [x['name'] for x in strategies]} + return {"strategies": [x["name"] for x in strategies]} -@router.get('/strategy/{strategy}', response_model=StrategyResponse, tags=['strategy']) +@router.get("/strategy/{strategy}", response_model=StrategyResponse, tags=["strategy"]) def get_strategy(strategy: str, config=Depends(get_config)): if ":" in strategy: raise HTTPException(status_code=500, detail="base64 encoded strategies are not allowed.") config_ = deepcopy(config) from freqtrade.resolvers.strategy_resolver import StrategyResolver + try: - strategy_obj = StrategyResolver._load_strategy(strategy, config_, - extra_dir=config_.get('strategy_path')) + strategy_obj = StrategyResolver._load_strategy( + strategy, config_, extra_dir=config_.get("strategy_path") + ) except OperationalException: - raise HTTPException(status_code=404, detail='Strategy not found') + raise HTTPException(status_code=404, detail="Strategy not found") except Exception as e: raise HTTPException(status_code=502, detail=str(e)) return { - 'strategy': strategy_obj.get_strategy_name(), - 'code': strategy_obj.__source__, - 'timeframe': getattr(strategy_obj, 'timeframe', None), + "strategy": strategy_obj.get_strategy_name(), + "code": strategy_obj.__source__, + "timeframe": getattr(strategy_obj, "timeframe", None), } -@router.get('/exchanges', response_model=ExchangeListResponse, tags=[]) +@router.get("/exchanges", response_model=ExchangeListResponse, tags=[]) def list_exchanges(config=Depends(get_config)): from freqtrade.exchange import list_available_exchanges + exchanges = list_available_exchanges(config) return { - 'exchanges': exchanges, + "exchanges": exchanges, } -@router.get('/freqaimodels', response_model=FreqAIModelListResponse, tags=['freqai']) +@router.get("/freqaimodels", response_model=FreqAIModelListResponse, tags=["freqai"]) def list_freqaimodels(config=Depends(get_config)): from freqtrade.resolvers.freqaimodel_resolver import FreqaiModelResolver - models = FreqaiModelResolver.search_all_objects( - config, False) - models = sorted(models, key=lambda x: x['name']) - return {'freqaimodels': [x['name'] for x in models]} + models = FreqaiModelResolver.search_all_objects(config, False) + models = sorted(models, key=lambda x: x["name"]) + + return {"freqaimodels": [x["name"] for x in models]} -@router.get('/available_pairs', response_model=AvailablePairs, tags=['candle data']) -def list_available_pairs(timeframe: Optional[str] = None, stake_currency: Optional[str] = None, - candletype: Optional[CandleType] = None, config=Depends(get_config)): - - dh = get_datahandler(config['datadir'], config.get('dataformat_ohlcv')) - trading_mode: TradingMode = config.get('trading_mode', TradingMode.SPOT) - pair_interval = dh.ohlcv_get_available_data(config['datadir'], trading_mode) +@router.get("/available_pairs", response_model=AvailablePairs, tags=["candle data"]) +def list_available_pairs( + timeframe: Optional[str] = None, + stake_currency: Optional[str] = None, + candletype: Optional[CandleType] = None, + config=Depends(get_config), +): + dh = get_datahandler(config["datadir"], config.get("dataformat_ohlcv")) + trading_mode: TradingMode = config.get("trading_mode", TradingMode.SPOT) + pair_interval = dh.ohlcv_get_available_data(config["datadir"], trading_mode) if timeframe: pair_interval = [pair for pair in pair_interval if pair[1] == timeframe] @@ -457,18 +492,18 @@ def list_available_pairs(timeframe: Optional[str] = None, stake_currency: Option pairs = list({x[0] for x in pair_interval}) pairs.sort() result = { - 'length': len(pairs), - 'pairs': pairs, - 'pair_interval': pair_interval, + "length": len(pairs), + "pairs": pairs, + "pair_interval": pair_interval, } return result -@router.get('/sysinfo', response_model=SysInfo, tags=['info']) +@router.get("/sysinfo", response_model=SysInfo, tags=["info"]) def sysinfo(): return RPC._rpc_sysinfo() -@router.get('/health', response_model=Health, tags=['info']) +@router.get("/health", response_model=Health, tags=["info"]) def health(rpc: RPC = Depends(get_rpc)): return rpc.health() diff --git a/freqtrade/rpc/api_server/api_ws.py b/freqtrade/rpc/api_server/api_ws.py index 1c8a2ba92..5e2eddc68 100644 --- a/freqtrade/rpc/api_server/api_ws.py +++ b/freqtrade/rpc/api_server/api_ws.py @@ -37,7 +37,7 @@ async def channel_reader(channel: WebSocketChannel, rpc: RPC): await _process_consumer_request(message, channel, rpc) except FreqtradeException: logger.exception(f"Error processing request from {channel}") - response = WSErrorMessage(data='Error processing request') + response = WSErrorMessage(data="Error processing request") await channel.send(response.dict(exclude_none=True)) @@ -47,23 +47,21 @@ async def channel_broadcaster(channel: WebSocketChannel, message_stream: Message Iterate over messages in the message stream and send them """ async for message, ts in message_stream: - if channel.subscribed_to(message.get('type')): + if channel.subscribed_to(message.get("type")): # Log a warning if this channel is behind # on the message stream by a lot if (time.time() - ts) > 60: - logger.warning(f"Channel {channel} is behind MessageStream by 1 minute," - " this can cause a memory leak if you see this message" - " often, consider reducing pair list size or amount of" - " consumers.") + logger.warning( + f"Channel {channel} is behind MessageStream by 1 minute," + " this can cause a memory leak if you see this message" + " often, consider reducing pair list size or amount of" + " consumers." + ) await channel.send(message, timeout=True) -async def _process_consumer_request( - request: Dict[str, Any], - channel: WebSocketChannel, - rpc: RPC -): +async def _process_consumer_request(request: Dict[str, Any], channel: WebSocketChannel, rpc: RPC): """ Validate and handle a request from a websocket consumer """ @@ -102,8 +100,8 @@ async def _process_consumer_request( elif type_ == RPCRequestType.ANALYZED_DF: # Limit the amount of candles per dataframe to 'limit' or 1500 - limit = int(min(data.get('limit', 1500), 1500)) if data else None - pair = data.get('pair', None) if data else None + limit = int(min(data.get("limit", 1500), 1500)) if data else None + pair = data.get("pair", None) if data else None # For every pair in the generator, send a separate message for message in rpc._ws_request_analyzed_df(limit, pair): @@ -117,11 +115,10 @@ async def message_endpoint( websocket: WebSocket, token: str = Depends(validate_ws_token), rpc: RPC = Depends(get_rpc), - message_stream: MessageStream = Depends(get_message_stream) + message_stream: MessageStream = Depends(get_message_stream), ): if token: async with create_channel(websocket) as channel: await channel.run_channel_tasks( - channel_reader(channel, rpc), - channel_broadcaster(channel, message_stream) + channel_reader(channel, rpc), channel_broadcaster(channel, message_stream) ) diff --git a/freqtrade/rpc/api_server/deps.py b/freqtrade/rpc/api_server/deps.py index c8c06695a..766673be7 100644 --- a/freqtrade/rpc/api_server/deps.py +++ b/freqtrade/rpc/api_server/deps.py @@ -20,7 +20,6 @@ def get_rpc_optional() -> Optional[RPC]: async def get_rpc() -> Optional[AsyncIterator[RPC]]: - _rpc = get_rpc_optional() if _rpc: request_id = str(uuid4()) @@ -33,7 +32,7 @@ async def get_rpc() -> Optional[AsyncIterator[RPC]]: _request_id_ctx_var.reset(ctx_token) else: - raise RPCException('Bot is not in the correct state') + raise RPCException("Bot is not in the correct state") def get_config() -> Dict[str, Any]: @@ -41,7 +40,7 @@ def get_config() -> Dict[str, Any]: def get_api_config() -> Dict[str, Any]: - return ApiServer._config['api_server'] + return ApiServer._config["api_server"] def _generate_exchange_key(config: Config) -> str: @@ -55,8 +54,8 @@ def get_exchange(config=Depends(get_config)): exchange_key = _generate_exchange_key(config) if not (exchange := ApiBG.exchanges.get(exchange_key)): from freqtrade.resolvers import ExchangeResolver - exchange = ExchangeResolver.load_exchange( - config, validate=False, load_leverage_tiers=False) + + exchange = ExchangeResolver.load_exchange(config, validate=False, load_leverage_tiers=False) ApiBG.exchanges[exchange_key] = exchange return exchange @@ -66,7 +65,6 @@ def get_message_stream(): def is_webserver_mode(config=Depends(get_config)): - if config['runmode'] != RunMode.WEBSERVER: - raise HTTPException(status_code=503, - detail='Bot is not in the correct state.') + if config["runmode"] != RunMode.WEBSERVER: + raise HTTPException(status_code=503, detail="Bot is not in the correct state.") return None diff --git a/freqtrade/rpc/api_server/uvicorn_threaded.py b/freqtrade/rpc/api_server/uvicorn_threaded.py index 48786bec2..cad8251db 100644 --- a/freqtrade/rpc/api_server/uvicorn_threaded.py +++ b/freqtrade/rpc/api_server/uvicorn_threaded.py @@ -14,6 +14,7 @@ def asyncio_setup() -> None: # pragma: no cover 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) @@ -42,7 +43,6 @@ class UvicornServer(uvicorn.Server): try: import uvloop # noqa except ImportError: # pragma: no cover - asyncio_setup() else: asyncio.set_event_loop(uvloop.new_event_loop()) @@ -55,7 +55,7 @@ class UvicornServer(uvicorn.Server): @contextlib.contextmanager def run_in_thread(self): - self.thread = threading.Thread(target=self.run, name='FTUvicorn') + self.thread = threading.Thread(target=self.run, name="FTUvicorn") self.thread.start() while not self.started: time.sleep(1e-3) diff --git a/freqtrade/rpc/api_server/web_ui.py b/freqtrade/rpc/api_server/web_ui.py index b701b4901..6d37ec308 100644 --- a/freqtrade/rpc/api_server/web_ui.py +++ b/freqtrade/rpc/api_server/web_ui.py @@ -9,20 +9,21 @@ from starlette.responses import FileResponse router_ui = APIRouter() -@router_ui.get('/favicon.ico', include_in_schema=False) +@router_ui.get("/favicon.ico", include_in_schema=False) async def favicon(): - return FileResponse(str(Path(__file__).parent / 'ui/favicon.ico')) + return FileResponse(str(Path(__file__).parent / "ui/favicon.ico")) -@router_ui.get('/fallback_file.html', include_in_schema=False) +@router_ui.get("/fallback_file.html", include_in_schema=False) async def fallback(): - return FileResponse(str(Path(__file__).parent / 'ui/fallback_file.html')) + return FileResponse(str(Path(__file__).parent / "ui/fallback_file.html")) -@router_ui.get('/ui_version', include_in_schema=False) +@router_ui.get("/ui_version", include_in_schema=False) async def ui_version(): from freqtrade.commands.deploy_commands import read_ui_version - uibase = Path(__file__).parent / 'ui/installed/' + + uibase = Path(__file__).parent / "ui/installed/" version = read_ui_version(uibase) return { @@ -40,26 +41,26 @@ def is_relative_to(path: Path, base: Path) -> bool: return False -@router_ui.get('/{rest_of_path:path}', include_in_schema=False) +@router_ui.get("/{rest_of_path:path}", include_in_schema=False) async def index_html(rest_of_path: str): """ Emulate path fallback to index.html. """ - if rest_of_path.startswith('api') or rest_of_path.startswith('.'): + if rest_of_path.startswith("api") or rest_of_path.startswith("."): raise HTTPException(status_code=404, detail="Not Found") - uibase = Path(__file__).parent / 'ui/installed/' + uibase = Path(__file__).parent / "ui/installed/" filename = uibase / rest_of_path # It's security relevant to check "relative_to". # Without this, Directory-traversal is possible. media_type: Optional[str] = None - if filename.suffix == '.js': + if filename.suffix == ".js": # Force text/javascript for .js files - Circumvent faulty system configuration - media_type = 'application/javascript' + media_type = "application/javascript" if filename.is_file() and is_relative_to(filename, uibase): return FileResponse(str(filename), media_type=media_type) - index_file = uibase / 'index.html' + index_file = uibase / "index.html" if not index_file.is_file(): - return FileResponse(str(uibase.parent / 'fallback_file.html')) + return FileResponse(str(uibase.parent / "fallback_file.html")) # Fall back to index.html, as indicated by vue router docs return FileResponse(str(index_file)) diff --git a/freqtrade/rpc/api_server/webserver.py b/freqtrade/rpc/api_server/webserver.py index aa08585ed..79909f96e 100644 --- a/freqtrade/rpc/api_server/webserver.py +++ b/freqtrade/rpc/api_server/webserver.py @@ -32,7 +32,6 @@ class FTJSONResponse(JSONResponse): class ApiServer(RPCHandler): - __instance = None __initialized = False @@ -61,13 +60,14 @@ class ApiServer(RPCHandler): ApiServer.__initialized = True - api_config = self._config['api_server'] + api_config = self._config["api_server"] - self.app = FastAPI(title="Freqtrade API", - docs_url='/docs' if api_config.get('enable_openapi', False) else None, - redoc_url=None, - default_response_class=FTJSONResponse, - ) + self.app = FastAPI( + title="Freqtrade API", + docs_url="/docs" if api_config.get("enable_openapi", False) else None, + redoc_url=None, + default_response_class=FTJSONResponse, + ) self.configure_app(self.app, self._config) self.start_api() @@ -80,10 +80,10 @@ class ApiServer(RPCHandler): ApiServer._has_rpc = True else: # This should not happen assuming we didn't mess up. - raise OperationalException('RPC Handler already attached.') + raise OperationalException("RPC Handler already attached.") def cleanup(self) -> None: - """ Cleanup pending module resources """ + """Cleanup pending module resources""" ApiServer._has_rpc = False del ApiServer._rpc if self._server and not self._standalone: @@ -109,8 +109,7 @@ class ApiServer(RPCHandler): def handle_rpc_exception(self, request, exc): logger.error(f"API Error calling: {exc}") return JSONResponse( - status_code=502, - content={'error': f"Error querying {request.url.path}: {exc.message}"} + status_code=502, content={"error": f"Error querying {request.url.path}: {exc.message}"} ) def configure_app(self, app: FastAPI, config): @@ -126,38 +125,36 @@ class ApiServer(RPCHandler): app.include_router(api_v1_public, prefix="/api/v1") app.include_router(router_login, prefix="/api/v1", tags=["auth"]) - app.include_router(api_v1, prefix="/api/v1", - dependencies=[Depends(http_basic_or_jwt_token)], - ) - app.include_router(api_backtest, prefix="/api/v1", - dependencies=[Depends(http_basic_or_jwt_token), - Depends(is_webserver_mode)], - ) - app.include_router(api_bg_tasks, prefix="/api/v1", - dependencies=[Depends(http_basic_or_jwt_token), - Depends(is_webserver_mode)], - ) + app.include_router( + api_v1, + prefix="/api/v1", + dependencies=[Depends(http_basic_or_jwt_token)], + ) + app.include_router( + api_backtest, + prefix="/api/v1", + dependencies=[Depends(http_basic_or_jwt_token), Depends(is_webserver_mode)], + ) + app.include_router( + api_bg_tasks, + prefix="/api/v1", + dependencies=[Depends(http_basic_or_jwt_token), Depends(is_webserver_mode)], + ) app.include_router(ws_router, prefix="/api/v1") # UI Router MUST be last! - app.include_router(router_ui, prefix='') + app.include_router(router_ui, prefix="") app.add_middleware( CORSMiddleware, - allow_origins=config['api_server'].get('CORS_origins', []), + allow_origins=config["api_server"].get("CORS_origins", []), allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) app.add_exception_handler(RPCException, self.handle_rpc_exception) - app.add_event_handler( - event_type="startup", - func=self._api_startup_event - ) - app.add_event_handler( - event_type="shutdown", - func=self._api_shutdown_event - ) + app.add_event_handler(event_type="startup", func=self._api_startup_event) + app.add_event_handler(event_type="shutdown", func=self._api_shutdown_event) async def _api_startup_event(self): """ @@ -179,35 +176,43 @@ class ApiServer(RPCHandler): """ Start API ... should be run in thread. """ - rest_ip = self._config['api_server']['listen_ip_address'] - rest_port = self._config['api_server']['listen_port'] + rest_ip = self._config["api_server"]["listen_ip_address"] + rest_port = self._config["api_server"]["listen_port"] - logger.info(f'Starting HTTP Server at {rest_ip}:{rest_port}') + logger.info(f"Starting HTTP Server at {rest_ip}:{rest_port}") if not IPv4Address(rest_ip).is_loopback and not running_in_docker(): logger.warning("SECURITY WARNING - Local Rest Server listening to external connections") - logger.warning("SECURITY WARNING - This is insecure please set to your loopback," - "e.g 127.0.0.1 in config.json") + logger.warning( + "SECURITY WARNING - This is insecure please set to your loopback," + "e.g 127.0.0.1 in config.json" + ) - if not self._config['api_server'].get('password'): - logger.warning("SECURITY WARNING - No password for local REST Server defined. " - "Please make sure that this is intentional!") + if not self._config["api_server"].get("password"): + logger.warning( + "SECURITY WARNING - No password for local REST Server defined. " + "Please make sure that this is intentional!" + ) - if (self._config['api_server'].get('jwt_secret_key', 'super-secret') - in ('super-secret, somethingrandom')): - logger.warning("SECURITY WARNING - `jwt_secret_key` seems to be default." - "Others may be able to log into your bot.") + if self._config["api_server"].get("jwt_secret_key", "super-secret") in ( + "super-secret, somethingrandom" + ): + logger.warning( + "SECURITY WARNING - `jwt_secret_key` seems to be default." + "Others may be able to log into your bot." + ) - logger.info('Starting Local Rest Server.') - verbosity = self._config['api_server'].get('verbosity', 'error') + logger.info("Starting Local Rest Server.") + verbosity = self._config["api_server"].get("verbosity", "error") - uvconfig = uvicorn.Config(self.app, - port=rest_port, - host=rest_ip, - use_colors=False, - log_config=None, - access_log=True if verbosity != 'error' else False, - ws_ping_interval=None # We do this explicitly ourselves - ) + uvconfig = uvicorn.Config( + self.app, + port=rest_port, + host=rest_ip, + use_colors=False, + log_config=None, + access_log=True if verbosity != "error" else False, + ws_ping_interval=None, # We do this explicitly ourselves + ) try: self._server = UvicornServer(uvconfig) if self._standalone: diff --git a/freqtrade/rpc/api_server/webserver_bgwork.py b/freqtrade/rpc/api_server/webserver_bgwork.py index 13f45227e..d3cf4d2ea 100644 --- a/freqtrade/rpc/api_server/webserver_bgwork.py +++ b/freqtrade/rpc/api_server/webserver_bgwork.py @@ -1,4 +1,3 @@ - from typing import Any, Dict, Literal, Optional, TypedDict from uuid import uuid4 @@ -6,7 +5,7 @@ from freqtrade.exchange.exchange import Exchange class JobsContainer(TypedDict): - category: Literal['pairlist'] + category: Literal["pairlist"] is_running: bool status: str progress: Optional[float] @@ -17,11 +16,11 @@ class JobsContainer(TypedDict): class ApiBG: # Backtesting type: Backtesting bt: Dict[str, Any] = { - 'bt': None, - 'data': None, - 'timerange': None, - 'last_config': {}, - 'bt_error': None, + "bt": None, + "data": None, + "timerange": None, + "last_config": {}, + "bt_error": None, } bgtask_running: bool = False # Exchange - only available in webserver mode. diff --git a/freqtrade/rpc/api_server/ws/channel.py b/freqtrade/rpc/api_server/ws/channel.py index 16cc883c1..0041bb6b2 100644 --- a/freqtrade/rpc/api_server/ws/channel.py +++ b/freqtrade/rpc/api_server/ws/channel.py @@ -25,12 +25,13 @@ class WebSocketChannel: """ Object to help facilitate managing a websocket connection """ + def __init__( self, websocket: WebSocketType, channel_id: Optional[str] = None, serializer_cls: Type[WebSocketSerializer] = HybridJSONWebSocketSerializer, - send_throttle: float = 0.01 + send_throttle: float = 0.01, ): self.channel_id = channel_id if channel_id else uuid4().hex[:8] self._websocket = WebSocketProxy(websocket) @@ -79,9 +80,7 @@ class WebSocketChannel: self._send_high_limit = min(max(self.avg_send_time * 2, 1), 3) async def send( - self, - message: Union[WSMessageSchemaType, Dict[str, Any]], - timeout: bool = False + self, message: Union[WSMessageSchemaType, Dict[str, Any]], timeout: bool = False ): """ Send a message on the wrapped websocket. If the sending @@ -97,8 +96,7 @@ class WebSocketChannel: # a TimeoutError and bubble up to the # message_endpoint to close the connection await asyncio.wait_for( - self._wrapped_ws.send(message), - timeout=self._send_high_limit if timeout else None + self._wrapped_ws.send(message), timeout=self._send_high_limit if timeout else None ) total_time = time.time() - _ self._send_times.append(total_time) @@ -207,7 +205,7 @@ class WebSocketChannel: asyncio.TimeoutError, WebSocketDisconnect, ConnectionClosed, - RuntimeError + RuntimeError, ): pass except Exception as e: @@ -227,10 +225,7 @@ class WebSocketChannel: @asynccontextmanager -async def create_channel( - websocket: WebSocketType, - **kwargs -) -> AsyncIterator[WebSocketChannel]: +async def create_channel(websocket: WebSocketType, **kwargs) -> AsyncIterator[WebSocketChannel]: """ Context manager for safely opening and closing a WebSocketChannel """ diff --git a/freqtrade/rpc/api_server/ws/message_stream.py b/freqtrade/rpc/api_server/ws/message_stream.py index a55a0da3c..f33bd7aef 100644 --- a/freqtrade/rpc/api_server/ws/message_stream.py +++ b/freqtrade/rpc/api_server/ws/message_stream.py @@ -7,6 +7,7 @@ class MessageStream: A message stream for consumers to subscribe to, and for producers to publish to. """ + def __init__(self): self._loop = asyncio.get_running_loop() self._waiter = self._loop.create_future() diff --git a/freqtrade/rpc/api_server/ws/serializer.py b/freqtrade/rpc/api_server/ws/serializer.py index 9a894e1bf..c07c6295f 100644 --- a/freqtrade/rpc/api_server/ws/serializer.py +++ b/freqtrade/rpc/api_server/ws/serializer.py @@ -46,15 +46,12 @@ class HybridJSONWebSocketSerializer(WebSocketSerializer): # Support serializing pandas DataFrames def _json_default(z): if isinstance(z, DataFrame): - return { - '__type__': 'dataframe', - '__value__': dataframe_to_json(z) - } + return {"__type__": "dataframe", "__value__": dataframe_to_json(z)} raise TypeError # Support deserializing JSON to pandas DataFrames def _json_object_hook(z): - if z.get('__type__') == 'dataframe': - return json_to_dataframe(z.get('__value__')) + if z.get("__type__") == "dataframe": + return json_to_dataframe(z.get("__value__")) return z diff --git a/freqtrade/rpc/api_server/ws_schemas.py b/freqtrade/rpc/api_server/ws_schemas.py index 970ea8cf8..70b12af8d 100644 --- a/freqtrade/rpc/api_server/ws_schemas.py +++ b/freqtrade/rpc/api_server/ws_schemas.py @@ -26,7 +26,7 @@ class WSMessageSchemaType(TypedDict): class WSMessageSchema(BaseArbitraryModel): type: RPCMessageType data: Optional[Any] = None - model_config = ConfigDict(extra='allow') + model_config = ConfigDict(extra="allow") # ------------------------------ REQUEST SCHEMAS ---------------------------- @@ -49,6 +49,7 @@ class WSAnalyzedDFRequest(WSRequestSchema): # ------------------------------ MESSAGE SCHEMAS ---------------------------- + class WSWhitelistMessage(WSMessageSchema): type: RPCMessageType = RPCMessageType.WHITELIST data: List[str] @@ -68,4 +69,5 @@ class WSErrorMessage(WSMessageSchema): type: RPCMessageType = RPCMessageType.EXCEPTION data: str + # -------------------------------------------------------------------------- diff --git a/freqtrade/rpc/discord.py b/freqtrade/rpc/discord.py index 43190e395..03f5fb2f8 100644 --- a/freqtrade/rpc/discord.py +++ b/freqtrade/rpc/discord.py @@ -10,18 +10,18 @@ logger = logging.getLogger(__name__) class Discord(Webhook): - def __init__(self, rpc: 'RPC', config: Config): + def __init__(self, rpc: "RPC", config: Config): self._config = config self.rpc = rpc - self.strategy = config.get('strategy', '') - self.timeframe = config.get('timeframe', '') - self.bot_name = config.get('bot_name', '') + self.strategy = config.get("strategy", "") + self.timeframe = config.get("timeframe", "") + self.bot_name = config.get("bot_name", "") - self._url = config['discord']['webhook_url'] - self._format = 'json' + self._url = config["discord"]["webhook_url"] + self._format = "json" self._retries = 1 self._retry_delay = 0.1 - self._timeout = self._config['discord'].get('timeout', 10) + self._timeout = self._config["discord"].get("timeout", 10) def cleanup(self) -> None: """ @@ -31,32 +31,31 @@ class Discord(Webhook): pass def send_msg(self, msg) -> None: - - if (fields := self._config['discord'].get(msg['type'].value)): + if fields := self._config["discord"].get(msg["type"].value): logger.info(f"Sending discord message: {msg}") - msg['strategy'] = self.strategy - msg['timeframe'] = self.timeframe - msg['bot_name'] = self.bot_name + msg["strategy"] = self.strategy + msg["timeframe"] = self.timeframe + msg["bot_name"] = self.bot_name color = 0x0000FF - if msg['type'] in (RPCMessageType.EXIT, RPCMessageType.EXIT_FILL): - profit_ratio = msg.get('profit_ratio') - color = (0x00FF00 if profit_ratio > 0 else 0xFF0000) - title = msg['type'].value - if 'pair' in msg: + if msg["type"] in (RPCMessageType.EXIT, RPCMessageType.EXIT_FILL): + profit_ratio = msg.get("profit_ratio") + color = 0x00FF00 if profit_ratio > 0 else 0xFF0000 + title = msg["type"].value + if "pair" in msg: title = f"Trade: {msg['pair']} {msg['type'].value}" - embeds = [{ - 'title': title, - 'color': color, - 'fields': [], - - }] + embeds = [ + { + "title": title, + "color": color, + "fields": [], + } + ] for f in fields: for k, v in f.items(): v = v.format(**msg) - embeds[0]['fields'].append( - {'name': k, 'value': v, 'inline': True}) + embeds[0]["fields"].append({"name": k, "value": v, "inline": True}) # Send the message to discord channel - payload = {'embeds': embeds} + payload = {"embeds": embeds} self._send_msg(payload) diff --git a/freqtrade/rpc/external_message_consumer.py b/freqtrade/rpc/external_message_consumer.py index fc7cf10c6..7d33efea6 100644 --- a/freqtrade/rpc/external_message_consumer.py +++ b/freqtrade/rpc/external_message_consumer.py @@ -4,6 +4,7 @@ ExternalMessageConsumer module Main purpose is to connect to external bot's message websocket to consume data from it """ + import asyncio import logging import socket @@ -55,11 +56,7 @@ class ExternalMessageConsumer: other freqtrade bot's """ - def __init__( - self, - config: Dict[str, Any], - dataprovider: DataProvider - ): + def __init__(self, config: Dict[str, Any], dataprovider: DataProvider): self._config = config self._dp = dataprovider @@ -69,21 +66,21 @@ class ExternalMessageConsumer: self._main_task = None self._sub_tasks = None - self._emc_config = self._config.get('external_message_consumer', {}) + self._emc_config = self._config.get("external_message_consumer", {}) - self.enabled = self._emc_config.get('enabled', False) - self.producers: List[Producer] = self._emc_config.get('producers', []) + self.enabled = self._emc_config.get("enabled", False) + self.producers: List[Producer] = self._emc_config.get("producers", []) - self.wait_timeout = self._emc_config.get('wait_timeout', 30) # in seconds - self.ping_timeout = self._emc_config.get('ping_timeout', 10) # in seconds - self.sleep_time = self._emc_config.get('sleep_time', 10) # in seconds + self.wait_timeout = self._emc_config.get("wait_timeout", 30) # in seconds + self.ping_timeout = self._emc_config.get("ping_timeout", 10) # in seconds + self.sleep_time = self._emc_config.get("sleep_time", 10) # in seconds # The amount of candles per dataframe on the initial request - self.initial_candle_limit = self._emc_config.get('initial_candle_limit', 1500) + self.initial_candle_limit = self._emc_config.get("initial_candle_limit", 1500) # Message size limit, in megabytes. Default 8mb, Use bitwise operator << 20 to convert # as the websockets client expects bytes. - self.message_size_limit = (self._emc_config.get('message_size_limit', 8) << 20) + self.message_size_limit = self._emc_config.get("message_size_limit", 8) << 20 # Setting these explicitly as they probably shouldn't be changed by a user # Unless we somehow integrate this with the strategy to allow creating @@ -94,7 +91,7 @@ class ExternalMessageConsumer: self._initial_requests: List[WSRequestSchema] = [ WSSubscribeRequest(data=self.topics), WSWhitelistRequest(), - WSAnalyzedDFRequest() + WSAnalyzedDFRequest(), ] # Specify which function to use for which RPCMessageType @@ -192,31 +189,24 @@ class ExternalMessageConsumer: """ while self._running: try: - host, port = producer['host'], producer['port'] - token = producer['ws_token'] - name = producer['name'] - scheme = 'wss' if producer.get('secure', False) else 'ws' + host, port = producer["host"], producer["port"] + token = producer["ws_token"] + name = producer["name"] + scheme = "wss" if producer.get("secure", False) else "ws" ws_url = f"{scheme}://{host}:{port}/api/v1/message/ws?token={token}" # This will raise InvalidURI if the url is bad async with websockets.connect( - ws_url, - max_size=self.message_size_limit, - ping_interval=None + ws_url, max_size=self.message_size_limit, ping_interval=None ) as ws: - async with create_channel( - ws, - channel_id=name, - send_throttle=0.5 - ) as channel: - + async with create_channel(ws, channel_id=name, send_throttle=0.5) as channel: # Create the message stream for this channel self._channel_streams[name] = MessageStream() # Run the channel tasks while connected await channel.run_channel_tasks( self._receive_messages(channel, producer, lock), - self._send_requests(channel, self._channel_streams[name]) + self._send_requests(channel, self._channel_streams[name]), ) except (websockets.exceptions.InvalidURI, ValueError) as e: @@ -227,7 +217,7 @@ class ExternalMessageConsumer: socket.gaierror, ConnectionRefusedError, websockets.exceptions.InvalidStatusCode, - websockets.exceptions.InvalidMessage + websockets.exceptions.InvalidMessage, ) as e: logger.error(f"Connection Refused - {e} retrying in {self.sleep_time}s") await asyncio.sleep(self.sleep_time) @@ -235,7 +225,7 @@ class ExternalMessageConsumer: except ( websockets.exceptions.ConnectionClosedError, - websockets.exceptions.ConnectionClosedOK + websockets.exceptions.ConnectionClosedOK, ): # Just keep trying to connect again indefinitely await asyncio.sleep(self.sleep_time) @@ -260,10 +250,7 @@ class ExternalMessageConsumer: await channel.send(request) async def _receive_messages( - self, - channel: WebSocketChannel, - producer: Producer, - lock: asyncio.Lock + self, channel: WebSocketChannel, producer: Producer, lock: asyncio.Lock ): """ Loop to handle receiving messages from a Producer @@ -274,10 +261,7 @@ class ExternalMessageConsumer: """ while self._running: try: - message = await asyncio.wait_for( - channel.recv(), - timeout=self.wait_timeout - ) + message = await asyncio.wait_for(channel.recv(), timeout=self.wait_timeout) try: async with lock: @@ -291,7 +275,7 @@ class ExternalMessageConsumer: try: # ping pong = await channel.ping() - latency = (await asyncio.wait_for(pong, timeout=self.ping_timeout) * 1000) + latency = await asyncio.wait_for(pong, timeout=self.ping_timeout) * 1000 logger.info(f"Connection to {channel} still alive, latency: {latency}ms") continue @@ -303,9 +287,7 @@ class ExternalMessageConsumer: raise def send_producer_request( - self, - producer_name: str, - request: Union[WSRequestSchema, Dict[str, Any]] + self, producer_name: str, request: Union[WSRequestSchema, Dict[str, Any]] ): """ Publish a message to the producer's message stream to be @@ -324,7 +306,7 @@ class ExternalMessageConsumer: """ Handles external messages from a Producer """ - producer_name = producer.get('name', 'default') + producer_name = producer.get("name", "default") try: producer_message = WSMessageSchema.model_validate(message) @@ -377,7 +359,7 @@ class ExternalMessageConsumer: return # If set, remove the Entry and Exit signals from the Producer - if self._emc_config.get('remove_entry_exit_signals', False): + if self._emc_config.get("remove_entry_exit_signals", False): df = remove_entry_exit_signals(df) logger.debug(f"Received {len(df)} candle(s) for {key}") @@ -388,8 +370,8 @@ class ExternalMessageConsumer: last_analyzed=la, timeframe=timeframe, candle_type=candle_type, - producer_name=producer_name - ) + producer_name=producer_name, + ) if not did_append: # We want an overlap in candles in case some data has changed @@ -397,20 +379,17 @@ class ExternalMessageConsumer: # Set to None for all candles if we missed a full df's worth of candles n_missing = n_missing if n_missing < FULL_DATAFRAME_THRESHOLD else 1500 - logger.warning(f"Holes in data or no existing df, requesting {n_missing} candles " - f"for {key} from `{producer_name}`") + logger.warning( + f"Holes in data or no existing df, requesting {n_missing} candles " + f"for {key} from `{producer_name}`" + ) self.send_producer_request( - producer_name, - WSAnalyzedDFRequest( - data={ - "limit": n_missing, - "pair": pair - } - ) + producer_name, WSAnalyzedDFRequest(data={"limit": n_missing, "pair": pair}) ) return logger.debug( f"Consumed message from `{producer_name}` " - f"of type `RPCMessageType.ANALYZED_DF` for {key}") + f"of type `RPCMessageType.ANALYZED_DF` for {key}" + ) diff --git a/freqtrade/rpc/fiat_convert.py b/freqtrade/rpc/fiat_convert.py index 2b44d0546..96758d296 100644 --- a/freqtrade/rpc/fiat_convert.py +++ b/freqtrade/rpc/fiat_convert.py @@ -21,14 +21,14 @@ logger = logging.getLogger(__name__) # Manually map symbol to ID for some common coins # with duplicate coingecko entries coingecko_mapping = { - 'eth': 'ethereum', - 'bnb': 'binancecoin', - 'sol': 'solana', - 'usdt': 'tether', - 'busd': 'binance-usd', - 'tusd': 'true-usd', - 'usdc': 'usd-coin', - 'btc': 'bitcoin' + "eth": "ethereum", + "bnb": "binancecoin", + "sol": "solana", + "usdt": "tether", + "busd": "binance-usd", + "tusd": "true-usd", + "usdc": "usd-coin", + "btc": "bitcoin", } @@ -38,6 +38,7 @@ class CryptoToFiatConverter(LoggingMixin): This object contains a list of pair Crypto, FIAT This object is also a Singleton """ + __instance = None _coingecko: CoinGeckoAPI = None _coinlistings: List[Dict] = [] @@ -71,7 +72,8 @@ class CryptoToFiatConverter(LoggingMixin): except RequestException as request_exception: if "429" in str(request_exception): logger.warning( - "Too many requests for CoinGecko API, backing off and trying again later.") + "Too many requests for CoinGecko API, backing off and trying again later." + ) # Set backoff timestamp to 60 seconds in the future self._backoff = datetime.now().timestamp() + 60 return @@ -80,9 +82,10 @@ class CryptoToFiatConverter(LoggingMixin): "Could not load FIAT Cryptocurrency map for the following problem: " f"{request_exception}" ) - except (Exception) as exception: + except Exception as exception: logger.error( - f"Could not load FIAT Cryptocurrency map for the following problem: {exception}") + f"Could not load FIAT Cryptocurrency map for the following problem: {exception}" + ) def _get_gecko_id(self, crypto_symbol): if not self._coinlistings: @@ -93,13 +96,13 @@ class CryptoToFiatConverter(LoggingMixin): return None else: return None - found = [x for x in self._coinlistings if x['symbol'].lower() == crypto_symbol] + found = [x for x in self._coinlistings if x["symbol"].lower() == crypto_symbol] if crypto_symbol in coingecko_mapping.keys(): - found = [x for x in self._coinlistings if x['id'] == coingecko_mapping[crypto_symbol]] + found = [x for x in self._coinlistings if x["id"] == coingecko_mapping[crypto_symbol]] if len(found) == 1: - return found[0]['id'] + return found[0]["id"] if len(found) > 0: # Wrong! @@ -130,26 +133,23 @@ class CryptoToFiatConverter(LoggingMixin): fiat_symbol = fiat_symbol.lower() inverse = False - if crypto_symbol == 'usd': + if crypto_symbol == "usd": # usd corresponds to "uniswap-state-dollar" for coingecko. # We'll therefore need to "swap" the currencies logger.info(f"reversing Rates {crypto_symbol}, {fiat_symbol}") crypto_symbol = fiat_symbol - fiat_symbol = 'usd' + fiat_symbol = "usd" inverse = True symbol = f"{crypto_symbol}/{fiat_symbol}" # Check if the fiat conversion you want is supported if not self._is_supported_fiat(fiat=fiat_symbol): - raise ValueError(f'The fiat {fiat_symbol} is not supported.') + raise ValueError(f"The fiat {fiat_symbol} is not supported.") price = self._pair_price.get(symbol, None) if not price: - price = self._find_price( - crypto_symbol=crypto_symbol, - fiat_symbol=fiat_symbol - ) + price = self._find_price(crypto_symbol=crypto_symbol, fiat_symbol=fiat_symbol) if inverse and price != 0.0: price = 1 / price self._pair_price[symbol] = price @@ -174,7 +174,7 @@ class CryptoToFiatConverter(LoggingMixin): """ # Check if the fiat conversion you want is supported if not self._is_supported_fiat(fiat=fiat_symbol): - raise ValueError(f'The fiat {fiat_symbol} is not supported.') + raise ValueError(f"The fiat {fiat_symbol} is not supported.") # No need to convert if both crypto and fiat are the same if crypto_symbol == fiat_symbol: @@ -185,16 +185,15 @@ class CryptoToFiatConverter(LoggingMixin): if not _gecko_id: # return 0 for unsupported stake currencies (fiat-convert should not break the bot) self.log_once( - f"unsupported crypto-symbol {crypto_symbol.upper()} - returning 0.0", - logger.warning) + f"unsupported crypto-symbol {crypto_symbol.upper()} - returning 0.0", logger.warning + ) return 0.0 try: return float( - self._coingecko.get_price( - ids=_gecko_id, - vs_currencies=fiat_symbol - )[_gecko_id][fiat_symbol] + self._coingecko.get_price(ids=_gecko_id, vs_currencies=fiat_symbol)[_gecko_id][ + fiat_symbol + ] ) except Exception as exception: logger.error("Error in _find_price: %s", exception) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 161a314be..3802114a4 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -1,6 +1,7 @@ """ This module contains class to define a RPC communications """ + import logging from abc import abstractmethod from datetime import date, datetime, timedelta, timezone @@ -61,14 +62,11 @@ class RPCException(Exception): return self.message def __json__(self): - return { - 'msg': self.message - } + return {"msg": self.message} class RPCHandler: - - def __init__(self, rpc: 'RPC', config: Config) -> None: + def __init__(self, rpc: "RPC", config: Config) -> None: """ Initializes RPCHandlers :param rpc: instance of RPC Helper class @@ -80,22 +78,23 @@ class RPCHandler: @property def name(self) -> str: - """ Returns the lowercase name of the implementation """ + """Returns the lowercase name of the implementation""" return self.__class__.__name__.lower() @abstractmethod def cleanup(self) -> None: - """ Cleanup pending module resources """ + """Cleanup pending module resources""" @abstractmethod def send_msg(self, msg: RPCSendMsg) -> None: - """ Sends a message to all registered rpc modules """ + """Sends a message to all registered rpc modules""" class RPC: """ RPC class can be used to have extra feature, like bot data, and access to DB data """ + # Bind _fiat_converter if needed _fiat_converter: Optional[CryptoToFiatConverter] = None @@ -107,58 +106,64 @@ class RPC: """ self._freqtrade = freqtrade self._config: Config = freqtrade.config - if self._config.get('fiat_display_currency'): + if self._config.get("fiat_display_currency"): self._fiat_converter = CryptoToFiatConverter() @staticmethod - def _rpc_show_config(config, botstate: Union[State, str], - strategy_version: Optional[str] = None) -> Dict[str, Any]: + def _rpc_show_config( + config, botstate: Union[State, str], strategy_version: Optional[str] = None + ) -> Dict[str, Any]: """ Return a dict of config options. Explicitly does NOT return the full config to avoid leakage of sensitive information via rpc. """ val = { - 'version': __version__, - 'strategy_version': strategy_version, - 'dry_run': config['dry_run'], - 'trading_mode': config.get('trading_mode', 'spot'), - 'short_allowed': config.get('trading_mode', 'spot') != 'spot', - 'stake_currency': config['stake_currency'], - 'stake_currency_decimals': decimals_per_coin(config['stake_currency']), - 'stake_amount': str(config['stake_amount']), - 'available_capital': config.get('available_capital'), - 'max_open_trades': (config.get('max_open_trades', 0) - if config.get('max_open_trades', 0) != float('inf') else -1), - 'minimal_roi': config['minimal_roi'].copy() if 'minimal_roi' in config else {}, - 'stoploss': config.get('stoploss'), - 'stoploss_on_exchange': config.get('order_types', - {}).get('stoploss_on_exchange', False), - 'trailing_stop': config.get('trailing_stop'), - 'trailing_stop_positive': config.get('trailing_stop_positive'), - 'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset'), - 'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached'), - 'unfilledtimeout': config.get('unfilledtimeout'), - 'use_custom_stoploss': config.get('use_custom_stoploss'), - 'order_types': config.get('order_types'), - 'bot_name': config.get('bot_name', 'freqtrade'), - 'timeframe': config.get('timeframe'), - 'timeframe_ms': timeframe_to_msecs(config['timeframe'] - ) if 'timeframe' in config else 0, - 'timeframe_min': timeframe_to_minutes(config['timeframe'] - ) if 'timeframe' in config else 0, - 'exchange': config['exchange']['name'], - 'strategy': config['strategy'], - 'force_entry_enable': config.get('force_entry_enable', False), - 'exit_pricing': config.get('exit_pricing', {}), - 'entry_pricing': config.get('entry_pricing', {}), - 'state': str(botstate), - 'runmode': config['runmode'].value, - 'position_adjustment_enable': config.get('position_adjustment_enable', False), - 'max_entry_position_adjustment': ( - config.get('max_entry_position_adjustment', -1) - if config.get('max_entry_position_adjustment') != float('inf') - else -1) + "version": __version__, + "strategy_version": strategy_version, + "dry_run": config["dry_run"], + "trading_mode": config.get("trading_mode", "spot"), + "short_allowed": config.get("trading_mode", "spot") != "spot", + "stake_currency": config["stake_currency"], + "stake_currency_decimals": decimals_per_coin(config["stake_currency"]), + "stake_amount": str(config["stake_amount"]), + "available_capital": config.get("available_capital"), + "max_open_trades": ( + config.get("max_open_trades", 0) + if config.get("max_open_trades", 0) != float("inf") + else -1 + ), + "minimal_roi": config["minimal_roi"].copy() if "minimal_roi" in config else {}, + "stoploss": config.get("stoploss"), + "stoploss_on_exchange": config.get("order_types", {}).get( + "stoploss_on_exchange", False + ), + "trailing_stop": config.get("trailing_stop"), + "trailing_stop_positive": config.get("trailing_stop_positive"), + "trailing_stop_positive_offset": config.get("trailing_stop_positive_offset"), + "trailing_only_offset_is_reached": config.get("trailing_only_offset_is_reached"), + "unfilledtimeout": config.get("unfilledtimeout"), + "use_custom_stoploss": config.get("use_custom_stoploss"), + "order_types": config.get("order_types"), + "bot_name": config.get("bot_name", "freqtrade"), + "timeframe": config.get("timeframe"), + "timeframe_ms": timeframe_to_msecs(config["timeframe"]) if "timeframe" in config else 0, + "timeframe_min": timeframe_to_minutes(config["timeframe"]) + if "timeframe" in config + else 0, + "exchange": config["exchange"]["name"], + "strategy": config["strategy"], + "force_entry_enable": config.get("force_entry_enable", False), + "exit_pricing": config.get("exit_pricing", {}), + "entry_pricing": config.get("entry_pricing", {}), + "state": str(botstate), + "runmode": config["runmode"].value, + "position_adjustment_enable": config.get("position_adjustment_enable", False), + "max_entry_position_adjustment": ( + config.get("max_entry_position_adjustment", -1) + if config.get("max_entry_position_adjustment") != float("inf") + else -1 + ), } return val @@ -174,7 +179,7 @@ class RPC: trades = Trade.get_open_trades() if not trades: - raise RPCException('no active trade') + raise RPCException("no active trade") else: results = [] for trade in trades: @@ -184,11 +189,11 @@ class RPC: # prepare open orders details oo_details: Optional[str] = "" oo_details_lst = [ - f'({oo.order_type} {oo.side} rem={oo.safe_remaining:.8f})' + f"({oo.order_type} {oo.side} rem={oo.safe_remaining:.8f})" for oo in trade.open_orders - if oo.ft_order_side not in ['stoploss'] + if oo.ft_order_side not in ["stoploss"] ] - oo_details = ', '.join(oo_details_lst) + oo_details = ", ".join(oo_details_lst) total_profit_abs = 0.0 total_profit_ratio: Optional[float] = None @@ -196,11 +201,11 @@ class RPC: if trade.is_open: try: current_rate = self._freqtrade.exchange.get_rate( - trade.pair, side='exit', is_short=trade.is_short, refresh=False) + trade.pair, side="exit", is_short=trade.is_short, refresh=False + ) except (ExchangeError, PricingError): current_rate = NAN if len(trade.select_filled_orders(trade.entry_side)) > 0: - current_profit = current_profit_abs = current_profit_fiat = NAN if not isnan(current_rate): prof = trade.calculate_profit(current_rate) @@ -221,13 +226,13 @@ class RPC: if not isnan(current_profit_abs) and self._fiat_converter: current_profit_fiat = self._fiat_converter.convert_amount( current_profit_abs, - self._freqtrade.config['stake_currency'], - self._freqtrade.config['fiat_display_currency'] + self._freqtrade.config["stake_currency"], + self._freqtrade.config["fiat_display_currency"], ) total_profit_fiat = self._fiat_converter.convert_amount( total_profit_abs, - self._freqtrade.config['stake_currency'], - self._freqtrade.config['fiat_display_currency'] + self._freqtrade.config["stake_currency"], + self._freqtrade.config["fiat_display_currency"], ) # Calculate guaranteed profit (in case of trailing stop) @@ -241,32 +246,35 @@ class RPC: stoploss_current_dist_ratio = stoploss_current_dist / current_rate trade_dict = trade.to_json() - trade_dict.update(dict( - close_profit=trade.close_profit if not trade.is_open else None, - current_rate=current_rate, - profit_ratio=current_profit, - profit_pct=round(current_profit * 100, 2), - profit_abs=current_profit_abs, - profit_fiat=current_profit_fiat, - total_profit_abs=total_profit_abs, - total_profit_fiat=total_profit_fiat, - total_profit_ratio=total_profit_ratio, - stoploss_current_dist=stoploss_current_dist, - stoploss_current_dist_ratio=round(stoploss_current_dist_ratio, 8), - stoploss_current_dist_pct=round(stoploss_current_dist_ratio * 100, 2), - stoploss_entry_dist=stoploss_entry_dist, - stoploss_entry_dist_ratio=round(stoploss_entry_dist_ratio, 8), - open_orders=oo_details - )) + trade_dict.update( + dict( + close_profit=trade.close_profit if not trade.is_open else None, + current_rate=current_rate, + profit_ratio=current_profit, + profit_pct=round(current_profit * 100, 2), + profit_abs=current_profit_abs, + profit_fiat=current_profit_fiat, + total_profit_abs=total_profit_abs, + total_profit_fiat=total_profit_fiat, + total_profit_ratio=total_profit_ratio, + stoploss_current_dist=stoploss_current_dist, + stoploss_current_dist_ratio=round(stoploss_current_dist_ratio, 8), + stoploss_current_dist_pct=round(stoploss_current_dist_ratio * 100, 2), + stoploss_entry_dist=stoploss_entry_dist, + stoploss_entry_dist_ratio=round(stoploss_entry_dist_ratio, 8), + open_orders=oo_details, + ) + ) results.append(trade_dict) return results - def _rpc_status_table(self, stake_currency: str, - fiat_display_currency: str) -> Tuple[List, List, float]: + 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 + nonspot = self._config.get("trading_mode", TradingMode.SPOT) != TradingMode.SPOT if not trades: - raise RPCException('no active trade') + raise RPCException("no active trade") else: trades_list = [] fiat_profit_sum = NAN @@ -274,53 +282,54 @@ class RPC: # calculate profit and send message to user try: current_rate = self._freqtrade.exchange.get_rate( - trade.pair, side='exit', is_short=trade.is_short, refresh=False) + trade.pair, side="exit", is_short=trade.is_short, refresh=False + ) except (PricingError, ExchangeError): current_rate = NAN trade_profit = NAN - profit_str = f'{NAN:.2%}' + profit_str = f"{NAN:.2%}" else: if trade.nr_of_successful_entries > 0: profit = trade.calculate_profit(current_rate) trade_profit = profit.profit_abs - profit_str = f'{profit.profit_ratio:.2%}' + profit_str = f"{profit.profit_ratio:.2%}" else: trade_profit = 0.0 - profit_str = f'{0.0:.2f}' - direction_str = ('S' if trade.is_short else 'L') if nonspot else '' + profit_str = f"{0.0:.2f}" + 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, - stake_currency, - fiat_display_currency + trade_profit, stake_currency, fiat_display_currency ) if not isnan(fiat_profit): profit_str += f" ({fiat_profit:.2f})" - fiat_profit_sum = fiat_profit if isnan(fiat_profit_sum) \ - else fiat_profit_sum + fiat_profit + fiat_profit_sum = ( + fiat_profit if isnan(fiat_profit_sum) else fiat_profit_sum + fiat_profit + ) else: profit_str += f" ({trade_profit:.2f})" - fiat_profit_sum = trade_profit if isnan(fiat_profit_sum) \ - else fiat_profit_sum + trade_profit + fiat_profit_sum = ( + trade_profit if isnan(fiat_profit_sum) else fiat_profit_sum + trade_profit + ) active_attempt_side_symbols = [ - '*' if (oo and oo.ft_order_side == trade.entry_side) else '**' + "*" if (oo and oo.ft_order_side == trade.entry_side) else "**" for oo in trade.open_orders ] # example: '*.**.**' trying to enter, exit and exit with 3 different orders - active_attempt_side_symbols_str = '.'.join(active_attempt_side_symbols) + active_attempt_side_symbols_str = ".".join(active_attempt_side_symbols) detail_trade = [ - f'{trade.id} {direction_str}', + f"{trade.id} {direction_str}", trade.pair + active_attempt_side_symbols_str, shorten_date(dt_humanize_delta(trade.open_date_utc)), - profit_str + profit_str, ] - if self._config.get('position_adjustment_enable', False): - max_entry_str = '' - if self._config.get('max_entry_position_adjustment', -1) > 0: + if self._config.get("position_adjustment_enable", False): + max_entry_str = "" + if self._config.get("max_entry_position_adjustment", -1) > 0: max_entry_str = f"/{self._config['max_entry_position_adjustment'] + 1}" filled_entries = trade.nr_of_successful_entries detail_trade.append(f"{filled_entries}{max_entry_str}") @@ -331,36 +340,35 @@ class RPC: else: profitcol += " (" + stake_currency + ")" - columns = [ - 'ID L/S' if nonspot else 'ID', - 'Pair', - 'Since', - profitcol] - if self._config.get('position_adjustment_enable', False): - columns.append('# Entries') + 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 def _rpc_timeunit_profit( - self, timescale: int, - stake_currency: str, fiat_display_currency: str, - timeunit: str = 'days') -> Dict[str, Any]: + self, + timescale: int, + stake_currency: str, + fiat_display_currency: str, + timeunit: str = "days", + ) -> Dict[str, Any]: """ :param timeunit: Valid entries are 'days', 'weeks', 'months' """ start_date = datetime.now(timezone.utc).date() - if timeunit == 'weeks': + if timeunit == "weeks": # weekly start_date = start_date - timedelta(days=start_date.weekday()) # Monday - if timeunit == 'months': + if timeunit == "months": start_date = start_date.replace(day=1) def time_offset(step: int): - if timeunit == 'months': + if timeunit == "months": return relativedelta(months=step) return timedelta(**{timeunit: step}) if not (isinstance(timescale, int) and timescale > 0): - raise RPCException('timescale must be an integer greater than 0') + raise RPCException("timescale must be an integer greater than 0") profit_units: Dict[date, Dict] = {} daily_stake = self._freqtrade.wallets.get_total_stake_amount() @@ -370,61 +378,66 @@ class RPC: # Only query for necessary columns for performance reasons. trades = Trade.session.execute( select(Trade.close_profit_abs) - .filter(Trade.is_open.is_(False), - Trade.close_date >= profitday, - Trade.close_date < (profitday + time_offset(1))) + .filter( + Trade.is_open.is_(False), + Trade.close_date >= profitday, + Trade.close_date < (profitday + time_offset(1)), + ) .order_by(Trade.close_date) ).all() curdayprofit = sum( - trade.close_profit_abs for trade in trades if trade.close_profit_abs is not None) + trade.close_profit_abs for trade in trades if trade.close_profit_abs is not None + ) # Calculate this periods starting balance daily_stake = daily_stake - curdayprofit profit_units[profitday] = { - 'amount': curdayprofit, - 'daily_stake': daily_stake, - 'rel_profit': round(curdayprofit / daily_stake, 8) if daily_stake > 0 else 0, - 'trades': len(trades), + "amount": curdayprofit, + "daily_stake": daily_stake, + "rel_profit": round(curdayprofit / daily_stake, 8) if daily_stake > 0 else 0, + "trades": len(trades), } data = [ { - 'date': key, - 'abs_profit': value["amount"], - 'starting_balance': value["daily_stake"], - 'rel_profit': value["rel_profit"], - 'fiat_value': self._fiat_converter.convert_amount( - value['amount'], - stake_currency, - fiat_display_currency - ) if self._fiat_converter else 0, - 'trade_count': value["trades"], + "date": key, + "abs_profit": value["amount"], + "starting_balance": value["daily_stake"], + "rel_profit": value["rel_profit"], + "fiat_value": self._fiat_converter.convert_amount( + value["amount"], stake_currency, fiat_display_currency + ) + if self._fiat_converter + else 0, + "trade_count": value["trades"], } for key, value in profit_units.items() ] return { - 'stake_currency': stake_currency, - 'fiat_display_currency': fiat_display_currency, - 'data': data + "stake_currency": stake_currency, + "fiat_display_currency": fiat_display_currency, + "data": data, } def _rpc_trade_history(self, limit: int, offset: int = 0, order_by_id: bool = False) -> Dict: - """ Returns the X last trades """ + """Returns the X last trades""" order_by: Any = Trade.id if order_by_id else Trade.close_date.desc() if limit: trades = Trade.session.scalars( Trade.get_trades_query([Trade.is_open.is_(False)]) .order_by(order_by) .limit(limit) - .offset(offset)) + .offset(offset) + ) else: trades = Trade.session.scalars( - Trade.get_trades_query([Trade.is_open.is_(False)]) - .order_by(Trade.close_date.desc())) + Trade.get_trades_query([Trade.is_open.is_(False)]).order_by(Trade.close_date.desc()) + ) output = [trade.to_json() for trade in trades] total_trades = Trade.session.scalar( - select(func.count(Trade.id)).filter(Trade.is_open.is_(False))) + select(func.count(Trade.id)).filter(Trade.is_open.is_(False)) + ) return { "trades": output, @@ -437,45 +450,49 @@ class RPC: """ Generate generic stats for trades in database """ + def trade_win_loss(trade): if trade.close_profit > 0: - return 'wins' + return "wins" elif trade.close_profit < 0: - return 'losses' + return "losses" else: - return 'draws' + return "draws" + trades = Trade.get_trades([Trade.is_open.is_(False)], include_orders=False) # Duration - dur: Dict[str, List[float]] = {'wins': [], 'draws': [], 'losses': []} + dur: Dict[str, List[float]] = {"wins": [], "draws": [], "losses": []} # Exit reason exit_reasons = {} for trade in trades: if trade.exit_reason not in exit_reasons: - exit_reasons[trade.exit_reason] = {'wins': 0, 'losses': 0, 'draws': 0} + exit_reasons[trade.exit_reason] = {"wins": 0, "losses": 0, "draws": 0} exit_reasons[trade.exit_reason][trade_win_loss(trade)] += 1 if trade.close_date is not None and trade.open_date is not None: trade_dur = (trade.close_date - trade.open_date).total_seconds() dur[trade_win_loss(trade)].append(trade_dur) - wins_dur = sum(dur['wins']) / len(dur['wins']) if len(dur['wins']) > 0 else None - draws_dur = sum(dur['draws']) / len(dur['draws']) if len(dur['draws']) > 0 else None - losses_dur = sum(dur['losses']) / len(dur['losses']) if len(dur['losses']) > 0 else None + wins_dur = sum(dur["wins"]) / len(dur["wins"]) if len(dur["wins"]) > 0 else None + draws_dur = sum(dur["draws"]) / len(dur["draws"]) if len(dur["draws"]) > 0 else None + losses_dur = sum(dur["losses"]) / len(dur["losses"]) if len(dur["losses"]) > 0 else None - durations = {'wins': wins_dur, 'draws': draws_dur, 'losses': losses_dur} - return {'exit_reasons': exit_reasons, 'durations': durations} + durations = {"wins": wins_dur, "draws": draws_dur, "losses": losses_dur} + return {"exit_reasons": exit_reasons, "durations": durations} def _rpc_trade_statistics( - self, stake_currency: str, fiat_display_currency: str, - start_date: Optional[datetime] = None) -> Dict[str, Any]: - """ Returns cumulative profit statistics """ + self, stake_currency: str, fiat_display_currency: str, start_date: Optional[datetime] = None + ) -> Dict[str, Any]: + """Returns cumulative profit statistics""" start_date = datetime.fromtimestamp(0) if start_date is None else start_date - trade_filter = ((Trade.is_open.is_(False) & (Trade.close_date >= start_date)) | - Trade.is_open.is_(True)) - trades: Sequence[Trade] = Trade.session.scalars(Trade.get_trades_query( - trade_filter, include_orders=False).order_by(Trade.id)).all() + trade_filter = ( + Trade.is_open.is_(False) & (Trade.close_date >= start_date) + ) | Trade.is_open.is_(True) + trades: Sequence[Trade] = Trade.session.scalars( + Trade.get_trades_query(trade_filter, include_orders=False).order_by(Trade.id) + ).all() profit_all_coin = [] profit_all_ratio = [] @@ -511,7 +528,8 @@ class RPC: continue try: current_rate = self._freqtrade.exchange.get_rate( - trade.pair, side='exit', is_short=trade.is_short, refresh=False) + trade.pair, side="exit", is_short=trade.is_short, refresh=False + ) except (PricingError, ExchangeError): current_rate = NAN profit_ratio = NAN @@ -535,11 +553,13 @@ class RPC: profit_closed_ratio_mean = float(mean(profit_closed_ratio) if profit_closed_ratio else 0.0) profit_closed_ratio_sum = sum(profit_closed_ratio) if profit_closed_ratio else 0.0 - profit_closed_fiat = self._fiat_converter.convert_amount( - profit_closed_coin_sum, - stake_currency, - fiat_display_currency - ) if self._fiat_converter else 0 + profit_closed_fiat = ( + self._fiat_converter.convert_amount( + profit_closed_coin_sum, stake_currency, fiat_display_currency + ) + if self._fiat_converter + else 0 + ) profit_all_coin_sum = round(sum(profit_all_coin), 8) profit_all_ratio_mean = float(mean(profit_all_ratio) if profit_all_ratio else 0.0) @@ -552,14 +572,21 @@ class RPC: profit_closed_ratio_fromstart = profit_closed_coin_sum / starting_balance profit_all_ratio_fromstart = profit_all_coin_sum / starting_balance - profit_factor = winning_profit / abs(losing_profit) if losing_profit else float('inf') + profit_factor = winning_profit / abs(losing_profit) if losing_profit else float("inf") winrate = (winning_trades / closed_trade_count) if closed_trade_count > 0 else 0 - trades_df = DataFrame([{'close_date': format_date(trade.close_date), - 'close_date_dt': trade.close_date, - 'profit_abs': trade.close_profit_abs} - for trade in trades if not trade.is_open and trade.close_date]) + trades_df = DataFrame( + [ + { + "close_date": format_date(trade.close_date), + "close_date_dt": trade.close_date, + "profit_abs": trade.close_profit_abs, + } + for trade in trades + if not trade.is_open and trade.close_date + ] + ) expectancy, expectancy_ratio = calculate_expectancy(trades_df) @@ -570,86 +597,97 @@ class RPC: dd_high_val = dd_low_val = 0.0 if len(trades_df) > 0: try: - (max_drawdown_abs, drawdown_start, drawdown_end, dd_high_val, dd_low_val, - max_drawdown) = calculate_max_drawdown( - trades_df, value_col='profit_abs', date_col='close_date_dt', - starting_balance=starting_balance) + ( + max_drawdown_abs, + drawdown_start, + drawdown_end, + dd_high_val, + dd_low_val, + max_drawdown, + ) = calculate_max_drawdown( + trades_df, + value_col="profit_abs", + date_col="close_date_dt", + starting_balance=starting_balance, + ) except ValueError: # ValueError if no losing trade. pass - profit_all_fiat = self._fiat_converter.convert_amount( - profit_all_coin_sum, - stake_currency, - fiat_display_currency - ) if self._fiat_converter else 0 + profit_all_fiat = ( + self._fiat_converter.convert_amount( + profit_all_coin_sum, stake_currency, fiat_display_currency + ) + if self._fiat_converter + else 0 + ) first_date = trades[0].open_date_utc if trades else None last_date = trades[-1].open_date_utc if trades else None num = float(len(durations) or 1) bot_start = KeyValueStore.get_datetime_value(KeyStoreKeys.BOT_START_TIME) return { - 'profit_closed_coin': profit_closed_coin_sum, - 'profit_closed_percent_mean': round(profit_closed_ratio_mean * 100, 2), - 'profit_closed_ratio_mean': profit_closed_ratio_mean, - 'profit_closed_percent_sum': round(profit_closed_ratio_sum * 100, 2), - 'profit_closed_ratio_sum': profit_closed_ratio_sum, - 'profit_closed_ratio': profit_closed_ratio_fromstart, - 'profit_closed_percent': round(profit_closed_ratio_fromstart * 100, 2), - 'profit_closed_fiat': profit_closed_fiat, - 'profit_all_coin': profit_all_coin_sum, - 'profit_all_percent_mean': round(profit_all_ratio_mean * 100, 2), - 'profit_all_ratio_mean': profit_all_ratio_mean, - 'profit_all_percent_sum': round(profit_all_ratio_sum * 100, 2), - 'profit_all_ratio_sum': profit_all_ratio_sum, - 'profit_all_ratio': profit_all_ratio_fromstart, - 'profit_all_percent': round(profit_all_ratio_fromstart * 100, 2), - 'profit_all_fiat': profit_all_fiat, - 'trade_count': len(trades), - 'closed_trade_count': closed_trade_count, - 'first_trade_date': format_date(first_date), - 'first_trade_humanized': dt_humanize_delta(first_date) if first_date else '', - 'first_trade_timestamp': dt_ts_def(first_date, 0), - 'latest_trade_date': format_date(last_date), - 'latest_trade_humanized': dt_humanize_delta(last_date) if last_date else '', - 'latest_trade_timestamp': dt_ts_def(last_date, 0), - 'avg_duration': str(timedelta(seconds=sum(durations) / num)).split('.')[0], - 'best_pair': best_pair[0] if best_pair else '', - 'best_rate': round(best_pair[1] * 100, 2) if best_pair else 0, # Deprecated - 'best_pair_profit_ratio': best_pair[1] if best_pair else 0, - 'winning_trades': winning_trades, - 'losing_trades': losing_trades, - 'profit_factor': profit_factor, - 'winrate': winrate, - 'expectancy': expectancy, - 'expectancy_ratio': expectancy_ratio, - 'max_drawdown': max_drawdown, - 'max_drawdown_abs': max_drawdown_abs, - 'max_drawdown_start': format_date(drawdown_start), - 'max_drawdown_start_timestamp': dt_ts_def(drawdown_start), - 'max_drawdown_end': format_date(drawdown_end), - 'max_drawdown_end_timestamp': dt_ts_def(drawdown_end), - 'drawdown_high': dd_high_val, - 'drawdown_low': dd_low_val, - 'trading_volume': trading_volume, - 'bot_start_timestamp': dt_ts_def(bot_start, 0), - 'bot_start_date': format_date(bot_start), + "profit_closed_coin": profit_closed_coin_sum, + "profit_closed_percent_mean": round(profit_closed_ratio_mean * 100, 2), + "profit_closed_ratio_mean": profit_closed_ratio_mean, + "profit_closed_percent_sum": round(profit_closed_ratio_sum * 100, 2), + "profit_closed_ratio_sum": profit_closed_ratio_sum, + "profit_closed_ratio": profit_closed_ratio_fromstart, + "profit_closed_percent": round(profit_closed_ratio_fromstart * 100, 2), + "profit_closed_fiat": profit_closed_fiat, + "profit_all_coin": profit_all_coin_sum, + "profit_all_percent_mean": round(profit_all_ratio_mean * 100, 2), + "profit_all_ratio_mean": profit_all_ratio_mean, + "profit_all_percent_sum": round(profit_all_ratio_sum * 100, 2), + "profit_all_ratio_sum": profit_all_ratio_sum, + "profit_all_ratio": profit_all_ratio_fromstart, + "profit_all_percent": round(profit_all_ratio_fromstart * 100, 2), + "profit_all_fiat": profit_all_fiat, + "trade_count": len(trades), + "closed_trade_count": closed_trade_count, + "first_trade_date": format_date(first_date), + "first_trade_humanized": dt_humanize_delta(first_date) if first_date else "", + "first_trade_timestamp": dt_ts_def(first_date, 0), + "latest_trade_date": format_date(last_date), + "latest_trade_humanized": dt_humanize_delta(last_date) if last_date else "", + "latest_trade_timestamp": dt_ts_def(last_date, 0), + "avg_duration": str(timedelta(seconds=sum(durations) / num)).split(".")[0], + "best_pair": best_pair[0] if best_pair else "", + "best_rate": round(best_pair[1] * 100, 2) if best_pair else 0, # Deprecated + "best_pair_profit_ratio": best_pair[1] if best_pair else 0, + "winning_trades": winning_trades, + "losing_trades": losing_trades, + "profit_factor": profit_factor, + "winrate": winrate, + "expectancy": expectancy, + "expectancy_ratio": expectancy_ratio, + "max_drawdown": max_drawdown, + "max_drawdown_abs": max_drawdown_abs, + "max_drawdown_start": format_date(drawdown_start), + "max_drawdown_start_timestamp": dt_ts_def(drawdown_start), + "max_drawdown_end": format_date(drawdown_end), + "max_drawdown_end_timestamp": dt_ts_def(drawdown_end), + "drawdown_high": dd_high_val, + "drawdown_low": dd_low_val, + "trading_volume": trading_volume, + "bot_start_timestamp": dt_ts_def(bot_start, 0), + "bot_start_date": format_date(bot_start), } def __balance_get_est_stake( - self, coin: str, stake_currency: str, amount: float, - balance: Wallet, tickers) -> Tuple[float, float]: + self, coin: str, stake_currency: str, amount: float, balance: Wallet, tickers + ) -> Tuple[float, float]: est_stake = 0.0 est_bot_stake = 0.0 if coin == stake_currency: est_stake = balance.total - if self._config.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT: + if self._config.get("trading_mode", TradingMode.SPOT) != TradingMode.SPOT: # in Futures, "total" includes the locked stake, and therefore all positions est_stake = balance.free est_bot_stake = amount else: pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency) - rate: Optional[float] = tickers.get(pair, {}).get('last', None) + rate: Optional[float] = tickers.get(pair, {}).get("last", None) if rate: if pair.startswith(stake_currency) and not pair.endswith(stake_currency): rate = 1.0 / rate @@ -659,21 +697,26 @@ class RPC: return est_stake, est_bot_stake def _rpc_balance(self, stake_currency: str, fiat_display_currency: str) -> Dict: - """ Returns current account balance per crypto """ + """Returns current account balance per crypto""" currencies: List[Dict] = [] total = 0.0 total_bot = 0.0 try: tickers: Tickers = self._freqtrade.exchange.get_tickers(cached=True) - except (ExchangeError): - raise RPCException('Error getting current tickers.') + except ExchangeError: + raise RPCException("Error getting current tickers.") open_trades: List[Trade] = Trade.get_open_trades() open_assets: Dict[str, Trade] = {t.safe_base_currency: t for t in open_trades} 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 + starting_cap_fiat = ( + self._fiat_converter.convert_amount( + starting_capital, stake_currency, fiat_display_currency + ) + if self._fiat_converter + else 0 + ) coin: str balance: Wallet for coin, balance in self._freqtrade.wallets.get_all_balances().items(): @@ -688,7 +731,8 @@ class RPC: try: est_stake, est_stake_bot = self.__balance_get_est_stake( - coin, stake_currency, trade_amount, balance, tickers) + coin, stake_currency, trade_amount, balance, tickers + ) except ValueError: continue @@ -696,89 +740,99 @@ class RPC: if is_bot_managed: total_bot += est_stake_bot - currencies.append({ - 'currency': coin, - 'free': balance.free, - 'balance': balance.total, - 'used': balance.used, - 'bot_owned': trade_amount, - 'est_stake': est_stake or 0, - 'est_stake_bot': est_stake_bot if is_bot_managed else 0, - 'stake': stake_currency, - 'side': 'long', - 'leverage': 1, - 'position': 0, - 'is_bot_managed': is_bot_managed, - 'is_position': False, - }) + currencies.append( + { + "currency": coin, + "free": balance.free, + "balance": balance.total, + "used": balance.used, + "bot_owned": trade_amount, + "est_stake": est_stake or 0, + "est_stake_bot": est_stake_bot if is_bot_managed else 0, + "stake": stake_currency, + "side": "long", + "leverage": 1, + "position": 0, + "is_bot_managed": is_bot_managed, + "is_position": False, + } + ) symbol: str position: PositionWallet for symbol, position in self._freqtrade.wallets.get_all_positions().items(): total += position.collateral total_bot += position.collateral - currencies.append({ - 'currency': symbol, - 'free': 0, - 'balance': 0, - 'used': 0, - 'position': position.position, - 'est_stake': position.collateral, - 'est_stake_bot': position.collateral, - 'stake': stake_currency, - 'leverage': position.leverage, - 'side': position.side, - 'is_bot_managed': True, - 'is_position': True - }) + currencies.append( + { + "currency": symbol, + "free": 0, + "balance": 0, + "used": 0, + "position": position.position, + "est_stake": position.collateral, + "est_stake_bot": position.collateral, + "stake": stake_currency, + "leverage": position.leverage, + "side": position.side, + "is_bot_managed": True, + "is_position": True, + } + ) - value = self._fiat_converter.convert_amount( - total, stake_currency, fiat_display_currency) if self._fiat_converter else 0 - value_bot = self._fiat_converter.convert_amount( - total_bot, stake_currency, fiat_display_currency) if self._fiat_converter else 0 + value = ( + self._fiat_converter.convert_amount(total, stake_currency, fiat_display_currency) + if self._fiat_converter + else 0 + ) + value_bot = ( + self._fiat_converter.convert_amount(total_bot, stake_currency, fiat_display_currency) + if self._fiat_converter + else 0 + ) trade_count = len(Trade.get_trades_proxy()) starting_capital_ratio = (total_bot / starting_capital) - 1 if starting_capital else 0.0 starting_cap_fiat_ratio = (value_bot / starting_cap_fiat) - 1 if starting_cap_fiat else 0.0 return { - 'currencies': currencies, - 'total': total, - 'total_bot': total_bot, - 'symbol': fiat_display_currency, - 'value': value, - 'value_bot': value_bot, - '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), - 'trade_count': trade_count, - 'note': 'Simulated balances' if self._freqtrade.config['dry_run'] else '' + "currencies": currencies, + "total": total, + "total_bot": total_bot, + "symbol": fiat_display_currency, + "value": value, + "value_bot": value_bot, + "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), + "trade_count": trade_count, + "note": "Simulated balances" if self._freqtrade.config["dry_run"] else "", } def _rpc_start(self) -> Dict[str, str]: - """ Handler for start """ + """Handler for start""" if self._freqtrade.state == State.RUNNING: - return {'status': 'already running'} + return {"status": "already running"} self._freqtrade.state = State.RUNNING - return {'status': 'starting trader ...'} + return {"status": "starting trader ..."} def _rpc_stop(self) -> Dict[str, str]: - """ Handler for stop """ + """Handler for stop""" if self._freqtrade.state == State.RUNNING: self._freqtrade.state = State.STOPPED - return {'status': 'stopping trader ...'} + return {"status": "stopping trader ..."} - return {'status': 'already stopped'} + return {"status": "already stopped"} def _rpc_reload_config(self) -> Dict[str, str]: - """ Handler for reload_config. """ + """Handler for reload_config.""" self._freqtrade.state = State.RELOAD_CONFIG - return {'status': 'Reloading config ...'} + return {"status": "Reloading config ..."} def _rpc_stopentry(self) -> Dict[str, str]: """ @@ -786,10 +840,10 @@ class RPC: """ if self._freqtrade.state == State.RUNNING: # Set 'max_open_trades' to 0 - self._freqtrade.config['max_open_trades'] = 0 + self._freqtrade.config["max_open_trades"] = 0 self._freqtrade.strategy.max_open_trades = 0 - return {'status': 'No more entries will occur from now. Run /reload_config to reset.'} + return {"status": "No more entries will occur from now. Run /reload_config to reset."} def _rpc_reload_trade_from_exchange(self, trade_id: int) -> Dict[str, str]: """ @@ -801,112 +855,126 @@ class RPC: raise RPCException(f"Could not find trade with id {trade_id}.") self._freqtrade.handle_onexchange_order(trade) - return {'status': 'Reloaded from orders from exchange'} + return {"status": "Reloaded from orders from exchange"} - def __exec_force_exit(self, trade: Trade, ordertype: Optional[str], - amount: Optional[float] = None) -> bool: + def __exec_force_exit( + self, trade: Trade, ordertype: Optional[str], amount: Optional[float] = None + ) -> bool: # Check if there is there are open orders trade_entry_cancelation_registry = [] for oo in trade.open_orders: - trade_entry_cancelation_res = {'order_id': oo.order_id, 'cancel_state': False} + trade_entry_cancelation_res = {"order_id": oo.order_id, "cancel_state": False} order = self._freqtrade.exchange.fetch_order(oo.order_id, trade.pair) - if order['side'] == trade.entry_side: + if order["side"] == trade.entry_side: fully_canceled = self._freqtrade.handle_cancel_enter( - trade, order, oo, CANCEL_REASON['FORCE_EXIT']) - trade_entry_cancelation_res['cancel_state'] = fully_canceled + trade, order, oo, CANCEL_REASON["FORCE_EXIT"] + ) + trade_entry_cancelation_res["cancel_state"] = fully_canceled trade_entry_cancelation_registry.append(trade_entry_cancelation_res) - if order['side'] == trade.exit_side: + if order["side"] == trade.exit_side: # Cancel order - so it is placed anew with a fresh price. - self._freqtrade.handle_cancel_exit( - trade, order, oo, CANCEL_REASON['FORCE_EXIT']) + self._freqtrade.handle_cancel_exit(trade, order, oo, CANCEL_REASON["FORCE_EXIT"]) - if all(tocr['cancel_state'] is False for tocr in trade_entry_cancelation_registry): + if all(tocr["cancel_state"] is False for tocr in trade_entry_cancelation_registry): if trade.has_open_orders: # Order cancellation failed, so we can't exit. return False # Get current rate and execute sell current_rate = self._freqtrade.exchange.get_rate( - trade.pair, side='exit', is_short=trade.is_short, refresh=True) + trade.pair, side="exit", is_short=trade.is_short, refresh=True + ) exit_check = ExitCheckTuple(exit_type=ExitType.FORCE_EXIT) order_type = ordertype or self._freqtrade.strategy.order_types.get( - "force_exit", self._freqtrade.strategy.order_types["exit"]) + "force_exit", self._freqtrade.strategy.order_types["exit"] + ) sub_amount: Optional[float] = None if amount and amount < trade.amount: # Partial exit ... min_exit_stake = self._freqtrade.exchange.get_min_pair_stake_amount( - trade.pair, current_rate, trade.stop_loss_pct) + trade.pair, current_rate, trade.stop_loss_pct + ) remaining = (trade.amount - amount) * current_rate if remaining < min_exit_stake: - raise RPCException(f'Remaining amount of {remaining} would be too small.') + raise RPCException(f"Remaining amount of {remaining} would be too small.") sub_amount = amount self._freqtrade.execute_trade_exit( - trade, current_rate, exit_check, ordertype=order_type, - sub_trade_amt=sub_amount) + trade, current_rate, exit_check, ordertype=order_type, sub_trade_amt=sub_amount + ) return True return False - def _rpc_force_exit(self, trade_id: str, ordertype: Optional[str] = None, *, - amount: Optional[float] = None) -> Dict[str, str]: + def _rpc_force_exit( + self, trade_id: str, ordertype: Optional[str] = None, *, amount: Optional[float] = None + ) -> Dict[str, str]: """ Handler for forceexit . Sells the given trade at current price """ if self._freqtrade.state != State.RUNNING: - raise RPCException('trader is not running') + raise RPCException("trader is not running") with self._freqtrade._exit_lock: - if trade_id == 'all': + if trade_id == "all": # Execute exit for all open orders for trade in Trade.get_open_trades(): self.__exec_force_exit(trade, ordertype) Trade.commit() self._freqtrade.wallets.update() - return {'result': 'Created exit orders for all open trades.'} + return {"result": "Created exit orders for all open trades."} # Query for trade trade = Trade.get_trades( - trade_filter=[Trade.id == trade_id, Trade.is_open.is_(True), ] + trade_filter=[ + Trade.id == trade_id, + Trade.is_open.is_(True), + ] ).first() if not trade: - logger.warning('force_exit: Invalid argument received') - raise RPCException('invalid argument') + logger.warning("force_exit: Invalid argument received") + raise RPCException("invalid argument") result = self.__exec_force_exit(trade, ordertype, amount) Trade.commit() self._freqtrade.wallets.update() if not result: - raise RPCException('Failed to exit trade.') - return {'result': f'Created exit order for trade {trade_id}.'} + raise RPCException("Failed to exit trade.") + return {"result": f"Created exit order for trade {trade_id}."} def _force_entry_validations(self, pair: str, order_side: SignalDirection): - if not self._freqtrade.config.get('force_entry_enable', False): - raise RPCException('Force_entry not enabled.') + if not self._freqtrade.config.get("force_entry_enable", False): + raise RPCException("Force_entry not enabled.") if self._freqtrade.state != State.RUNNING: - raise RPCException('trader is not running') + raise RPCException("trader is not running") if order_side == SignalDirection.SHORT and self._freqtrade.trading_mode == TradingMode.SPOT: raise RPCException("Can't go short on Spot markets.") if pair not in self._freqtrade.exchange.get_markets(tradable_only=True): - raise RPCException('Symbol does not exist or market is not active.') + raise RPCException("Symbol does not exist or market is not active.") # Check if pair quote currency equals to the stake currency. - stake_currency = self._freqtrade.config.get('stake_currency') + stake_currency = self._freqtrade.config.get("stake_currency") if not self._freqtrade.exchange.get_pair_quote_currency(pair) == stake_currency: raise RPCException( - f'Wrong pair selected. Only pairs with stake-currency {stake_currency} allowed.') + f"Wrong pair selected. Only pairs with stake-currency {stake_currency} allowed." + ) - def _rpc_force_entry(self, pair: str, price: Optional[float], *, - order_type: Optional[str] = None, - order_side: SignalDirection = SignalDirection.LONG, - stake_amount: Optional[float] = None, - enter_tag: Optional[str] = 'force_entry', - leverage: Optional[float] = None) -> Optional[Trade]: + def _rpc_force_entry( + self, + pair: str, + price: Optional[float], + *, + order_type: Optional[str] = None, + order_side: SignalDirection = SignalDirection.LONG, + stake_amount: Optional[float] = None, + enter_tag: Optional[str] = "force_entry", + leverage: Optional[float] = None, + ) -> Optional[Trade]: """ Handler for forcebuy Buys a pair trade at the given or current price @@ -917,56 +985,68 @@ class RPC: # check if pair already has an open pair trade: Optional[Trade] = Trade.get_trades( - [Trade.is_open.is_(True), Trade.pair == pair]).first() - is_short = (order_side == SignalDirection.SHORT) + [Trade.is_open.is_(True), Trade.pair == pair] + ).first() + is_short = order_side == SignalDirection.SHORT if trade: is_short = trade.is_short if not self._freqtrade.strategy.position_adjustment_enable: raise RPCException(f"position for {pair} already open - id: {trade.id}") if trade.has_open_orders: - raise RPCException(f"position for {pair} already open - id: {trade.id} " - f"and has open order {','.join(trade.open_orders_ids)}") + raise RPCException( + f"position for {pair} already open - id: {trade.id} " + f"and has open order {','.join(trade.open_orders_ids)}" + ) else: - if Trade.get_open_trade_count() >= self._config['max_open_trades']: + if Trade.get_open_trade_count() >= self._config["max_open_trades"]: raise RPCException("Maximum number of trades is reached.") if not stake_amount: # gen stake amount stake_amount = self._freqtrade.wallets.get_trade_stake_amount( - pair, self._config['max_open_trades']) + pair, self._config["max_open_trades"] + ) # execute buy if not order_type: order_type = self._freqtrade.strategy.order_types.get( - 'force_entry', self._freqtrade.strategy.order_types['entry']) + "force_entry", self._freqtrade.strategy.order_types["entry"] + ) with self._freqtrade._exit_lock: - if self._freqtrade.execute_entry(pair, stake_amount, price, - ordertype=order_type, trade=trade, - is_short=is_short, - enter_tag=enter_tag, - leverage_=leverage, - mode='pos_adjust' if trade else 'initial' - ): + if self._freqtrade.execute_entry( + pair, + stake_amount, + price, + ordertype=order_type, + trade=trade, + is_short=is_short, + enter_tag=enter_tag, + leverage_=leverage, + mode="pos_adjust" if trade else "initial", + ): Trade.commit() trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first() return trade else: - raise RPCException(f'Failed to enter position for {pair}.') + raise RPCException(f"Failed to enter position for {pair}.") def _rpc_cancel_open_order(self, trade_id: int): if self._freqtrade.state != State.RUNNING: - raise RPCException('trader is not running') + raise RPCException("trader is not running") with self._freqtrade._exit_lock: # Query for trade trade = Trade.get_trades( - trade_filter=[Trade.id == trade_id, Trade.is_open.is_(True), ] + trade_filter=[ + Trade.id == trade_id, + Trade.is_open.is_(True), + ] ).first() if not trade: - logger.warning('cancel_open_order: Invalid trade_id received.') - raise RPCException('Invalid trade_id.') + logger.warning("cancel_open_order: Invalid trade_id received.") + raise RPCException("Invalid trade_id.") if not trade.has_open_orders: - logger.warning('cancel_open_order: No open order for trade_id.') - raise RPCException('No open order for trade_id.') + logger.warning("cancel_open_order: No open order for trade_id.") + raise RPCException("No open order for trade_id.") for open_order in trade.open_orders: try: @@ -975,7 +1055,8 @@ class RPC: logger.info(f"Cannot query order for {trade} due to {e}.", exc_info=True) raise RPCException("Order not found.") self._freqtrade.handle_cancel_order( - order, open_order, trade, CANCEL_REASON['USER_CANCEL']) + order, open_order, trade, CANCEL_REASON["USER_CANCEL"] + ) Trade.commit() def _rpc_delete(self, trade_id: int) -> Dict[str, Union[str, int]]: @@ -987,35 +1068,36 @@ class RPC: c_count = 0 trade = Trade.get_trades(trade_filter=[Trade.id == trade_id]).first() if not trade: - logger.warning('delete trade: Invalid argument received') - raise RPCException('invalid argument') + logger.warning("delete trade: Invalid argument received") + raise RPCException("invalid argument") # Try cancelling regular order if that exists for open_order in trade.open_orders: try: self._freqtrade.exchange.cancel_order(open_order.order_id, trade.pair) c_count += 1 - except (ExchangeError): + except ExchangeError: pass # cancel stoploss on exchange orders ... - if (self._freqtrade.strategy.order_types.get('stoploss_on_exchange') - and trade.has_open_sl_orders): - + if ( + self._freqtrade.strategy.order_types.get("stoploss_on_exchange") + and trade.has_open_sl_orders + ): for oslo in trade.open_sl_orders: try: self._freqtrade.exchange.cancel_stoploss_order(oslo.order_id, trade.pair) c_count += 1 - except (ExchangeError): + except ExchangeError: pass trade.delete() self._freqtrade.wallets.update() return { - 'result': 'success', - 'trade_id': trade_id, - 'result_msg': f'Deleted trade {trade_id}. Closed {c_count} open orders.', - 'cancel_order_count': c_count, + "result": "success", + "trade_id": trade_id, + "result_msg": f"Deleted trade {trade_id}. Closed {c_count} open orders.", + "cancel_order_count": c_count, } def _rpc_list_custom_data(self, trade_id: int, key: Optional[str]) -> List[Dict[str, Any]]: @@ -1033,13 +1115,13 @@ class RPC: custom_data = trade.get_all_custom_data() return [ { - 'id': data_entry.id, - 'ft_trade_id': data_entry.ft_trade_id, - 'cd_key': data_entry.cd_key, - 'cd_type': data_entry.cd_type, - 'cd_value': data_entry.cd_value, - 'created_at': data_entry.created_at, - 'updated_at': data_entry.updated_at + "id": data_entry.id, + "ft_trade_id": data_entry.ft_trade_id, + "cd_key": data_entry.cd_key, + "cd_type": data_entry.cd_type, + "cd_value": data_entry.cd_value, + "created_at": data_entry.created_at, + "updated_at": data_entry.updated_at, } for data_entry in custom_data ] @@ -1077,30 +1159,31 @@ class RPC: return mix_tags def _rpc_count(self) -> Dict[str, float]: - """ Returns the number of trades running """ + """Returns the number of trades running""" if self._freqtrade.state != State.RUNNING: - raise RPCException('trader is not running') + raise RPCException("trader is not running") trades = Trade.get_open_trades() return { - 'current': len(trades), - 'max': (int(self._freqtrade.config['max_open_trades']) - if self._freqtrade.config['max_open_trades'] != float('inf') else -1), - 'total_stake': sum((trade.open_rate * trade.amount) for trade in trades) + "current": len(trades), + "max": ( + int(self._freqtrade.config["max_open_trades"]) + if self._freqtrade.config["max_open_trades"] != float("inf") + else -1 + ), + "total_stake": sum((trade.open_rate * trade.amount) for trade in trades), } def _rpc_locks(self) -> Dict[str, Any]: - """ Returns the current locks """ + """Returns the current locks""" locks = PairLocks.get_pair_locks(None) - return { - 'lock_count': len(locks), - 'locks': [lock.to_json() for lock in locks] - } + return {"lock_count": len(locks), "locks": [lock.to_json() for lock in locks]} - def _rpc_delete_lock(self, lockid: Optional[int] = None, - pair: Optional[str] = None) -> Dict[str, Any]: - """ Delete specific lock(s) """ + def _rpc_delete_lock( + self, lockid: Optional[int] = None, pair: Optional[str] = None + ) -> Dict[str, Any]: + """Delete specific lock(s)""" locks: Sequence[PairLock] = [] if pair: @@ -1117,7 +1200,8 @@ class RPC: return self._rpc_locks() def _rpc_add_lock( - self, pair: str, until: datetime, reason: Optional[str], side: str) -> PairLock: + self, pair: str, until: datetime, reason: Optional[str], side: str + ) -> PairLock: lock = PairLocks.lock_pair( pair=pair, until=until, @@ -1127,29 +1211,28 @@ class RPC: return lock def _rpc_whitelist(self) -> Dict: - """ Returns the currently active whitelist""" - res = {'method': self._freqtrade.pairlists.name_list, - 'length': len(self._freqtrade.active_pair_whitelist), - 'whitelist': self._freqtrade.active_pair_whitelist - } + """Returns the currently active whitelist""" + res = { + "method": self._freqtrade.pairlists.name_list, + "length": len(self._freqtrade.active_pair_whitelist), + "whitelist": self._freqtrade.active_pair_whitelist, + } return res def _rpc_blacklist_delete(self, delete: List[str]) -> Dict: - """ Removes pairs from currently active blacklist """ + """Removes pairs from currently active blacklist""" errors = {} for pair in delete: if pair in self._freqtrade.pairlists.blacklist: self._freqtrade.pairlists.blacklist.remove(pair) else: - errors[pair] = { - 'error_msg': f"Pair {pair} is not in the current blacklist." - } + errors[pair] = {"error_msg": f"Pair {pair} is not in the current blacklist."} resp = self._rpc_blacklist() - resp['errors'] = errors + resp["errors"] = errors return resp def _rpc_blacklist(self, add: Optional[List[str]] = None) -> Dict: - """ Returns the currently active blacklist""" + """Returns the currently active blacklist""" errors = {} if add: for pair in add: @@ -1159,18 +1242,17 @@ class RPC: self._freqtrade.pairlists.blacklist.append(pair) except ValueError: - errors[pair] = { - 'error_msg': f'Pair {pair} is not a valid wildcard.'} + errors[pair] = {"error_msg": f"Pair {pair} is not a valid wildcard."} else: - errors[pair] = { - 'error_msg': f'Pair {pair} already in pairlist.'} + errors[pair] = {"error_msg": f"Pair {pair} already in pairlist."} - res = {'method': self._freqtrade.pairlists.name_list, - 'length': len(self._freqtrade.pairlists.blacklist), - 'blacklist': self._freqtrade.pairlists.blacklist, - 'blacklist_expanded': self._freqtrade.pairlists.expanded_blacklist, - 'errors': errors, - } + res = { + "method": self._freqtrade.pairlists.name_list, + "length": len(self._freqtrade.pairlists.blacklist), + "blacklist": self._freqtrade.pairlists.blacklist, + "blacklist_expanded": self._freqtrade.pairlists.expanded_blacklist, + "errors": errors, + } return res @staticmethod @@ -1180,35 +1262,46 @@ class RPC: buffer = bufferHandler.buffer[-limit:] else: buffer = bufferHandler.buffer - records = [[format_date(datetime.fromtimestamp(r.created)), - r.created * 1000, r.name, r.levelname, - r.message + ('\n' + r.exc_text if r.exc_text else '')] - for r in buffer] + records = [ + [ + format_date(datetime.fromtimestamp(r.created)), + r.created * 1000, + r.name, + r.levelname, + r.message + ("\n" + r.exc_text if r.exc_text else ""), + ] + for r in buffer + ] # Log format: # [logtime-formatted, logepoch, logger-name, loglevel, message \n + exception] # e.g. ["2020-08-27 11:35:01", 1598520901097.9397, # "freqtrade.worker", "INFO", "Starting worker develop"] - return {'log_count': len(records), 'logs': records} + return {"log_count": len(records), "logs": records} def _rpc_edge(self) -> List[Dict[str, Any]]: - """ Returns information related to Edge """ + """Returns information related to Edge""" if not self._freqtrade.edge: - raise RPCException('Edge is not enabled.') + raise RPCException("Edge is not enabled.") return self._freqtrade.edge.accepted_pairs() @staticmethod def _convert_dataframe_to_dict( - strategy: str, pair: str, timeframe: str, dataframe: DataFrame, - last_analyzed: datetime, selected_cols: Optional[List[str]]) -> Dict[str, Any]: + strategy: str, + pair: str, + timeframe: str, + dataframe: DataFrame, + last_analyzed: datetime, + selected_cols: Optional[List[str]], + ) -> Dict[str, Any]: has_content = len(dataframe) != 0 dataframe_columns = list(dataframe.columns) signals = { - 'enter_long': 0, - 'exit_long': 0, - 'enter_short': 0, - 'exit_short': 0, + "enter_long": 0, + "exit_long": 0, + "enter_short": 0, + "exit_short": 0, } if has_content: if selected_cols is not None: @@ -1217,17 +1310,17 @@ class RPC: df_cols = [col for col in dataframe_columns if col in cols_set] dataframe = dataframe.loc[:, df_cols] - dataframe.loc[:, '__date_ts'] = dataframe.loc[:, 'date'].astype(int64) // 1000 // 1000 + dataframe.loc[:, "__date_ts"] = dataframe.loc[:, "date"].astype(int64) // 1000 // 1000 # Move signal close to separate column when signal for easy plotting for sig_type in signals.keys(): if sig_type in dataframe.columns: - mask = (dataframe[sig_type] == 1) + mask = dataframe[sig_type] == 1 signals[sig_type] = int(mask.sum()) - dataframe.loc[mask, f'_{sig_type}_signal_close'] = dataframe.loc[mask, 'close'] + dataframe.loc[mask, f"_{sig_type}_signal_close"] = dataframe.loc[mask, "close"] # band-aid until this is fixed: # https://github.com/pandas-dev/pandas/issues/45836 - datetime_types = ['datetime', 'datetime64', 'datetime64[ns, UTC]'] + datetime_types = ["datetime", "datetime64", "datetime64[ns, UTC]"] date_columns = dataframe.select_dtypes(include=datetime_types) for date_column in date_columns: # replace NaT with `None` @@ -1236,52 +1329,50 @@ class RPC: dataframe = dataframe.replace({inf: None, -inf: None, NAN: None}) res = { - 'pair': pair, - 'timeframe': timeframe, - 'timeframe_ms': timeframe_to_msecs(timeframe), - 'strategy': strategy, - 'all_columns': dataframe_columns, - 'columns': list(dataframe.columns), - 'data': dataframe.values.tolist(), - 'length': len(dataframe), - 'buy_signals': signals['enter_long'], # Deprecated - 'sell_signals': signals['exit_long'], # Deprecated - 'enter_long_signals': signals['enter_long'], - 'exit_long_signals': signals['exit_long'], - 'enter_short_signals': signals['enter_short'], - 'exit_short_signals': signals['exit_short'], - 'last_analyzed': last_analyzed, - 'last_analyzed_ts': int(last_analyzed.timestamp()), - 'data_start': '', - 'data_start_ts': 0, - 'data_stop': '', - 'data_stop_ts': 0, + "pair": pair, + "timeframe": timeframe, + "timeframe_ms": timeframe_to_msecs(timeframe), + "strategy": strategy, + "all_columns": dataframe_columns, + "columns": list(dataframe.columns), + "data": dataframe.values.tolist(), + "length": len(dataframe), + "buy_signals": signals["enter_long"], # Deprecated + "sell_signals": signals["exit_long"], # Deprecated + "enter_long_signals": signals["enter_long"], + "exit_long_signals": signals["exit_long"], + "enter_short_signals": signals["enter_short"], + "exit_short_signals": signals["exit_short"], + "last_analyzed": last_analyzed, + "last_analyzed_ts": int(last_analyzed.timestamp()), + "data_start": "", + "data_start_ts": 0, + "data_stop": "", + "data_stop_ts": 0, } if has_content: - res.update({ - 'data_start': str(dataframe.iloc[0]['date']), - 'data_start_ts': int(dataframe.iloc[0]['__date_ts']), - 'data_stop': str(dataframe.iloc[-1]['date']), - 'data_stop_ts': int(dataframe.iloc[-1]['__date_ts']), - }) + res.update( + { + "data_start": str(dataframe.iloc[0]["date"]), + "data_start_ts": int(dataframe.iloc[0]["__date_ts"]), + "data_stop": str(dataframe.iloc[-1]["date"]), + "data_stop_ts": int(dataframe.iloc[-1]["__date_ts"]), + } + ) return res def _rpc_analysed_dataframe( - self, pair: str, timeframe: str, limit: Optional[int], - selected_cols: Optional[List[str]]) -> Dict[str, Any]: - """ Analyzed dataframe in Dict form """ + self, pair: str, timeframe: str, limit: Optional[int], selected_cols: Optional[List[str]] + ) -> Dict[str, Any]: + """Analyzed dataframe in Dict form""" _data, last_analyzed = self.__rpc_analysed_dataframe_raw(pair, timeframe, limit) return RPC._convert_dataframe_to_dict( - self._freqtrade.config['strategy'], pair, timeframe, _data, last_analyzed, - selected_cols + self._freqtrade.config["strategy"], pair, timeframe, _data, last_analyzed, selected_cols ) def __rpc_analysed_dataframe_raw( - self, - pair: str, - timeframe: str, - limit: Optional[int] + self, pair: str, timeframe: str, limit: Optional[int] ) -> Tuple[DataFrame, datetime]: """ Get the dataframe and last analyze from the dataprovider @@ -1290,8 +1381,7 @@ class RPC: :param timeframe: The timeframe of data to get :param limit: The amount of candles in the dataframe """ - _data, last_analyzed = self._freqtrade.dataprovider.get_analyzed_dataframe( - pair, timeframe) + _data, last_analyzed = self._freqtrade.dataprovider.get_analyzed_dataframe(pair, timeframe) _data = _data.copy() if limit: @@ -1300,9 +1390,7 @@ class RPC: return _data, last_analyzed def _ws_all_analysed_dataframes( - self, - pairlist: List[str], - limit: Optional[int] + self, pairlist: List[str], limit: Optional[int] ) -> Generator[Dict[str, Any], None, None]: """ Get the analysed dataframes of each pair in the pairlist. @@ -1314,36 +1402,29 @@ class RPC: If a list of string date times, only returns those candles :returns: A generator of dictionaries with the key, dataframe, and last analyzed timestamp """ - timeframe = self._freqtrade.config['timeframe'] - candle_type = self._freqtrade.config.get('candle_type_def', CandleType.SPOT) + timeframe = self._freqtrade.config["timeframe"] + candle_type = self._freqtrade.config.get("candle_type_def", CandleType.SPOT) for pair in pairlist: dataframe, last_analyzed = self.__rpc_analysed_dataframe_raw(pair, timeframe, limit) - yield { - "key": (pair, timeframe, candle_type), - "df": dataframe, - "la": last_analyzed - } + yield {"key": (pair, timeframe, candle_type), "df": dataframe, "la": last_analyzed} - def _ws_request_analyzed_df( - self, - limit: Optional[int] = None, - pair: Optional[str] = None - ): - """ Historical Analyzed Dataframes for WebSocket """ + def _ws_request_analyzed_df(self, limit: Optional[int] = None, pair: Optional[str] = None): + """Historical Analyzed Dataframes for WebSocket""" pairlist = [pair] if pair else self._freqtrade.active_pair_whitelist return self._ws_all_analysed_dataframes(pairlist, limit) def _ws_request_whitelist(self): - """ Whitelist data for WebSocket """ + """Whitelist data for WebSocket""" return self._freqtrade.active_pair_whitelist @staticmethod - def _rpc_analysed_history_full(config: Config, pair: str, timeframe: str, - exchange, selected_cols: Optional[List[str]]) -> Dict[str, Any]: - timerange_parsed = TimeRange.parse_timerange(config.get('timerange')) + def _rpc_analysed_history_full( + config: Config, pair: str, timeframe: str, exchange, selected_cols: Optional[List[str]] + ) -> Dict[str, Any]: + timerange_parsed = TimeRange.parse_timerange(config.get("timerange")) from freqtrade.data.converter import trim_dataframe from freqtrade.data.dataprovider import DataProvider @@ -1357,45 +1438,53 @@ class RPC: pairs=[pair], timeframe=timeframe, timerange=timerange_parsed, - data_format=config['dataformat_ohlcv'], - candle_type=config.get('candle_type_def', CandleType.SPOT), + data_format=config["dataformat_ohlcv"], + candle_type=config.get("candle_type_def", CandleType.SPOT), startup_candles=startup_candles, ) if pair not in _data: raise RPCException( - f"No data for {pair}, {timeframe} in {config.get('timerange')} found.") + f"No data for {pair}, {timeframe} in {config.get('timerange')} found." + ) strategy.dp = DataProvider(config, exchange=exchange, pairlists=None) strategy.ft_bot_start() - df_analyzed = strategy.analyze_ticker(_data[pair], {'pair': pair}) + df_analyzed = strategy.analyze_ticker(_data[pair], {"pair": pair}) df_analyzed = trim_dataframe(df_analyzed, timerange_parsed, startup_candles=startup_candles) - return RPC._convert_dataframe_to_dict(strategy.get_strategy_name(), pair, timeframe, - df_analyzed.copy(), dt_now(), - selected_cols) + return RPC._convert_dataframe_to_dict( + strategy.get_strategy_name(), + pair, + timeframe, + df_analyzed.copy(), + dt_now(), + selected_cols, + ) def _rpc_plot_config(self) -> Dict[str, Any]: - if (self._freqtrade.strategy.plot_config and - 'subplots' not in self._freqtrade.strategy.plot_config): - self._freqtrade.strategy.plot_config['subplots'] = {} + if ( + self._freqtrade.strategy.plot_config + and "subplots" not in self._freqtrade.strategy.plot_config + ): + self._freqtrade.strategy.plot_config["subplots"] = {} return self._freqtrade.strategy.plot_config @staticmethod def _rpc_plot_config_with_strategy(config: Config) -> Dict[str, Any]: - from freqtrade.resolvers.strategy_resolver import StrategyResolver + strategy = StrategyResolver.load_strategy(config) - if (strategy.plot_config and 'subplots' not in strategy.plot_config): - strategy.plot_config['subplots'] = {} + if strategy.plot_config and "subplots" not in strategy.plot_config: + strategy.plot_config["subplots"] = {} return 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 + "ram_pct": psutil.virtual_memory().percent, } def health(self) -> Dict[str, Optional[Union[str, int]]]: @@ -1413,24 +1502,30 @@ class RPC: } if last_p is not None: - res.update({ - "last_process": str(last_p), - "last_process_loc": format_date(last_p.astimezone(tzlocal())), - "last_process_ts": int(last_p.timestamp()), - }) + res.update( + { + "last_process": str(last_p), + "last_process_loc": format_date(last_p.astimezone(tzlocal())), + "last_process_ts": int(last_p.timestamp()), + } + ) - if (bot_start := KeyValueStore.get_datetime_value(KeyStoreKeys.BOT_START_TIME)): - res.update({ - "bot_start": str(bot_start), - "bot_start_loc": format_date(bot_start.astimezone(tzlocal())), - "bot_start_ts": int(bot_start.timestamp()), - }) - if (bot_startup := KeyValueStore.get_datetime_value(KeyStoreKeys.STARTUP_TIME)): - res.update({ - "bot_startup": str(bot_startup), - "bot_startup_loc": format_date(bot_startup.astimezone(tzlocal())), - "bot_startup_ts": int(bot_startup.timestamp()), - }) + if bot_start := KeyValueStore.get_datetime_value(KeyStoreKeys.BOT_START_TIME): + res.update( + { + "bot_start": str(bot_start), + "bot_start_loc": format_date(bot_start.astimezone(tzlocal())), + "bot_start_ts": int(bot_start.timestamp()), + } + ) + if bot_startup := KeyValueStore.get_datetime_value(KeyStoreKeys.STARTUP_TIME): + res.update( + { + "bot_startup": str(bot_startup), + "bot_startup_loc": format_date(bot_startup.astimezone(tzlocal())), + "bot_startup_ts": int(bot_startup.timestamp()), + } + ) return res diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index 1972ad6e5..3e49b5ab5 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -1,6 +1,7 @@ """ This module contains class to manage RPC communications (Telegram, API, ...) """ + import logging from collections import deque from typing import List @@ -20,42 +21,46 @@ class RPCManager: """ def __init__(self, freqtrade) -> None: - """ Initializes all enabled rpc modules """ + """Initializes all enabled rpc modules""" self.registered_modules: List[RPCHandler] = [] self._rpc = RPC(freqtrade) config = freqtrade.config # Enable telegram - if config.get('telegram', {}).get('enabled', False): - logger.info('Enabling rpc.telegram ...') + if config.get("telegram", {}).get("enabled", False): + logger.info("Enabling rpc.telegram ...") from freqtrade.rpc.telegram import Telegram + self.registered_modules.append(Telegram(self._rpc, config)) # Enable discord - if config.get('discord', {}).get('enabled', False): - logger.info('Enabling rpc.discord ...') + if config.get("discord", {}).get("enabled", False): + logger.info("Enabling rpc.discord ...") from freqtrade.rpc.discord import Discord + self.registered_modules.append(Discord(self._rpc, config)) # Enable Webhook - if config.get('webhook', {}).get('enabled', False): - logger.info('Enabling rpc.webhook ...') + if config.get("webhook", {}).get("enabled", False): + logger.info("Enabling rpc.webhook ...") from freqtrade.rpc.webhook import Webhook + self.registered_modules.append(Webhook(self._rpc, config)) # Enable local rest api server for cmd line control - if config.get('api_server', {}).get('enabled', False): - logger.info('Enabling rpc.api_server') + if config.get("api_server", {}).get("enabled", False): + logger.info("Enabling rpc.api_server") from freqtrade.rpc.api_server import ApiServer + apiserver = ApiServer(config) apiserver.add_rpc_handler(self._rpc) self.registered_modules.append(apiserver) def cleanup(self) -> None: - """ Stops all enabled rpc modules """ - logger.info('Cleaning up rpc modules ...') + """Stops all enabled rpc modules""" + logger.info("Cleaning up rpc modules ...") while self.registered_modules: mod = self.registered_modules.pop() - logger.info('Cleaning up rpc.%s ...', mod.name) + logger.info("Cleaning up rpc.%s ...", mod.name) mod.cleanup() del mod @@ -68,16 +73,16 @@ class RPCManager: 'status': 'stopping bot' } """ - if msg.get('type') not in NO_ECHO_MESSAGES: - logger.info('Sending rpc message: %s', msg) + if msg.get("type") not in NO_ECHO_MESSAGES: + logger.info("Sending rpc message: %s", msg) for mod in self.registered_modules: - logger.debug('Forwarding message to rpc.%s', mod.name) + logger.debug("Forwarding message to rpc.%s", mod.name) try: mod.send_msg(msg) except NotImplementedError: logger.error(f"Message type '{msg['type']}' not implemented by handler {mod.name}.") except Exception: - logger.exception('Exception occurred within RPC module %s', mod.name) + logger.exception("Exception occurred within RPC module %s", mod.name) def process_msg_queue(self, queue: deque) -> None: """ @@ -85,47 +90,54 @@ class RPCManager: """ while queue: msg = queue.popleft() - logger.info('Sending rpc strategy_msg: %s', msg) + logger.info("Sending rpc strategy_msg: %s", msg) for mod in self.registered_modules: - if mod._config.get(mod.name, {}).get('allow_custom_messages', False): - mod.send_msg({ - 'type': RPCMessageType.STRATEGY_MSG, - 'msg': msg, - }) + if mod._config.get(mod.name, {}).get("allow_custom_messages", False): + mod.send_msg( + { + "type": RPCMessageType.STRATEGY_MSG, + "msg": msg, + } + ) def startup_messages(self, config: Config, pairlist, protections) -> None: - if config['dry_run']: - self.send_msg({ - 'type': RPCMessageType.WARNING, - 'status': 'Dry run is enabled. All trades are simulated.' - }) - stake_currency = config['stake_currency'] - stake_amount = config['stake_amount'] - minimal_roi = config['minimal_roi'] - stoploss = config['stoploss'] - trailing_stop = config['trailing_stop'] - timeframe = config['timeframe'] - exchange_name = config['exchange']['name'] - strategy_name = config.get('strategy', '') - pos_adjust_enabled = 'On' if config['position_adjustment_enable'] else 'Off' - self.send_msg({ - 'type': RPCMessageType.STARTUP, - 'status': f'*Exchange:* `{exchange_name}`\n' - f'*Stake per trade:* `{stake_amount} {stake_currency}`\n' - f'*Minimum ROI:* `{minimal_roi}`\n' - f'*{"Trailing " if trailing_stop else ""}Stoploss:* `{stoploss}`\n' - f'*Position adjustment:* `{pos_adjust_enabled}`\n' - f'*Timeframe:* `{timeframe}`\n' - f'*Strategy:* `{strategy_name}`' - }) - self.send_msg({ - 'type': RPCMessageType.STARTUP, - 'status': f'Searching for {stake_currency} pairs to buy and sell ' - f'based on {pairlist.short_desc()}' - }) + if config["dry_run"]: + self.send_msg( + { + "type": RPCMessageType.WARNING, + "status": "Dry run is enabled. All trades are simulated.", + } + ) + stake_currency = config["stake_currency"] + stake_amount = config["stake_amount"] + minimal_roi = config["minimal_roi"] + stoploss = config["stoploss"] + trailing_stop = config["trailing_stop"] + timeframe = config["timeframe"] + exchange_name = config["exchange"]["name"] + strategy_name = config.get("strategy", "") + pos_adjust_enabled = "On" if config["position_adjustment_enable"] else "Off" + self.send_msg( + { + "type": RPCMessageType.STARTUP, + "status": f'*Exchange:* `{exchange_name}`\n' + f'*Stake per trade:* `{stake_amount} {stake_currency}`\n' + f'*Minimum ROI:* `{minimal_roi}`\n' + f'*{"Trailing " if trailing_stop else ""}Stoploss:* `{stoploss}`\n' + f'*Position adjustment:* `{pos_adjust_enabled}`\n' + f'*Timeframe:* `{timeframe}`\n' + f'*Strategy:* `{strategy_name}`', + } + ) + self.send_msg( + { + "type": RPCMessageType.STARTUP, + "status": f"Searching for {stake_currency} pairs to buy and sell " + f"based on {pairlist.short_desc()}", + } + ) if len(protections.name_list) > 0: - prots = '\n'.join([p for prot in protections.short_desc() for k, p in prot.items()]) - self.send_msg({ - 'type': RPCMessageType.STARTUP, - 'status': f'Using Protections: \n{prots}' - }) + prots = "\n".join([p for prot in protections.short_desc() for k, p in prot.items()]) + self.send_msg( + {"type": RPCMessageType.STARTUP, "status": f"Using Protections: \n{prots}"} + ) diff --git a/freqtrade/rpc/rpc_types.py b/freqtrade/rpc/rpc_types.py index 72a382f48..e5f4f93c9 100644 --- a/freqtrade/rpc/rpc_types.py +++ b/freqtrade/rpc/rpc_types.py @@ -15,12 +15,14 @@ class RPCSendMsgBase(TypedDict): class RPCStatusMsg(RPCSendMsgBase): """Used for Status, Startup and Warning messages""" + type: Literal[RPCMessageType.STATUS, RPCMessageType.STARTUP, RPCMessageType.WARNING] status: str class RPCStrategyMsg(RPCSendMsgBase): """Used for Status, Startup and Warning messages""" + type: Literal[RPCMessageType.STRATEGY_MSG] msg: str @@ -108,12 +110,14 @@ class _AnalyzedDFData(TypedDict): class RPCAnalyzedDFMsg(RPCSendMsgBase): """New Analyzed dataframe message""" + type: Literal[RPCMessageType.ANALYZED_DF] data: _AnalyzedDFData class RPCNewCandleMsg(RPCSendMsgBase): """New candle ping message, issued once per new candle/pair""" + type: Literal[RPCMessageType.NEW_CANDLE] data: PairWithTimeframe @@ -131,5 +135,5 @@ RPCSendMsg = Union[ RPCExitMsg, RPCExitCancelMsg, RPCAnalyzedDFMsg, - RPCNewCandleMsg - ] + RPCNewCandleMsg, +] diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 5b9cf2763..ef8013ff7 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -3,6 +3,7 @@ """ This module manage Telegram communication """ + import asyncio import json import logging @@ -47,7 +48,7 @@ MAX_MESSAGE_LENGTH = MessageLimit.MAX_TEXT_LENGTH logger = logging.getLogger(__name__) -logger.debug('Included module rpc.telegram ...') +logger.debug("Included module rpc.telegram ...") def safe_async_db(func: Callable[..., Any]): @@ -56,9 +57,10 @@ def safe_async_db(func: Callable[..., Any]): :param func: function to decorate :return: decorated function """ + @wraps(func) def wrapper(*args, **kwargs): - """ Decorator logic """ + """Decorator logic""" try: return func(*args, **kwargs) finally: @@ -86,8 +88,8 @@ def authorized_only(command_handler: Callable[..., Coroutine[Any, Any, None]]): @wraps(command_handler) async def wrapper(self, *args, **kwargs): - """ Decorator logic """ - update = kwargs.get('update') or args[0] + """Decorator logic""" + update = kwargs.get("update") or args[0] # Reject unauthorized messages if update.callback_query: @@ -95,23 +97,19 @@ def authorized_only(command_handler: Callable[..., Coroutine[Any, Any, None]]): else: cchat_id = int(update.message.chat_id) - chat_id = int(self._config['telegram']['chat_id']) + chat_id = int(self._config["telegram"]["chat_id"]) if cchat_id != chat_id: - logger.info(f'Rejected unauthorized message from: {update.message.chat_id}') + logger.info(f"Rejected unauthorized message from: {update.message.chat_id}") return wrapper # Rollback session to avoid getting data stored in a transaction. Trade.rollback() - logger.debug( - 'Executing handler: %s for chat_id: %s', - command_handler.__name__, - chat_id - ) + logger.debug("Executing handler: %s for chat_id: %s", command_handler.__name__, chat_id) try: return await command_handler(self, *args, **kwargs) except RPCException as e: await self._send_msg(str(e)) except BaseException: - logger.exception('Exception occurred within Telegram module') + logger.exception("Exception occurred within Telegram module") finally: Trade.session.remove() @@ -119,7 +117,7 @@ def authorized_only(command_handler: Callable[..., Coroutine[Any, Any, None]]): class Telegram(RPCHandler): - """ This class handles all telegram communication """ + """This class handles all telegram communication""" def __init__(self, rpc: RPC, config: Config) -> None: """ @@ -139,7 +137,7 @@ class Telegram(RPCHandler): """ Creates and starts the polling thread """ - self._thread = Thread(target=self._init, name='FTTelegram') + self._thread = Thread(target=self._init, name="FTTelegram") self._thread.start() def _init_keyboard(self) -> None: @@ -148,51 +146,83 @@ class Telegram(RPCHandler): section. """ self._keyboard: List[List[Union[str, KeyboardButton]]] = [ - ['/daily', '/profit', '/balance'], - ['/status', '/status table', '/performance'], - ['/count', '/start', '/stop', '/help'] + ["/daily", "/profit", "/balance"], + ["/status", "/status table", "/performance"], + ["/count", "/start", "/stop", "/help"], ] # do not allow commands with mandatory arguments and critical cmds # 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()). valid_keys: List[str] = [ - r'/start$', r'/stop$', r'/status$', r'/status table$', - r'/trades$', r'/performance$', r'/buys', r'/entries', - r'/sells', r'/exits', r'/mix_tags', - r'/daily$', r'/daily \d+$', r'/profit$', r'/profit \d+', - r'/stats$', r'/count$', r'/locks$', r'/balance$', - r'/stopbuy$', r'/stopentry$', r'/reload_config$', r'/show_config$', - r'/logs$', r'/whitelist$', r'/whitelist(\ssorted|\sbaseonly)+$', - 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$', r'/marketdir (long|short|even|none)$', - r'/marketdir$' + r"/start$", + r"/stop$", + r"/status$", + r"/status table$", + r"/trades$", + r"/performance$", + r"/buys", + r"/entries", + r"/sells", + r"/exits", + r"/mix_tags", + r"/daily$", + r"/daily \d+$", + r"/profit$", + r"/profit \d+", + r"/stats$", + r"/count$", + r"/locks$", + r"/balance$", + r"/stopbuy$", + r"/stopentry$", + r"/reload_config$", + r"/show_config$", + r"/logs$", + r"/whitelist$", + r"/whitelist(\ssorted|\sbaseonly)+$", + 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$", + r"/marketdir (long|short|even|none)$", + r"/marketdir$", ] # Create keys for generation - valid_keys_print = [k.replace('$', '') for k in valid_keys] + valid_keys_print = [k.replace("$", "") for k in valid_keys] # custom keyboard specified in config.json - cust_keyboard = self._config['telegram'].get('keyboard', []) + cust_keyboard = self._config["telegram"].get("keyboard", []) if cust_keyboard: combined = "(" + ")|(".join(valid_keys) + ")" # check for valid shortcuts - invalid_keys = [b for b in chain.from_iterable(cust_keyboard) - if not re.match(combined, b)] + invalid_keys = [ + b for b in chain.from_iterable(cust_keyboard) if not re.match(combined, b) + ] if len(invalid_keys): - err_msg = ('config.telegram.keyboard: Invalid commands for ' - f'custom Telegram keyboard: {invalid_keys}' - f'\nvalid commands are: {valid_keys_print}') + err_msg = ( + "config.telegram.keyboard: Invalid commands for " + f"custom Telegram keyboard: {invalid_keys}" + f"\nvalid commands are: {valid_keys_print}" + ) raise OperationalException(err_msg) else: self._keyboard = cust_keyboard - logger.info('using custom keyboard from ' - f'config.json: {self._keyboard}') + logger.info("using custom keyboard from " f"config.json: {self._keyboard}") def _init_telegram_app(self): - return Application.builder().token(self._config['telegram']['token']).build() + return Application.builder().token(self._config["telegram"]["token"]).build() def _init(self) -> None: """ @@ -211,60 +241,65 @@ class Telegram(RPCHandler): # Register command handler and start telegram message polling handles = [ - CommandHandler('status', self._status), - CommandHandler('profit', self._profit), - CommandHandler('balance', self._balance), - CommandHandler('start', self._start), - CommandHandler('stop', self._stop), - CommandHandler(['forcesell', 'forceexit', 'fx'], self._force_exit), - CommandHandler(['forcebuy', 'forcelong'], partial( - self._force_enter, order_side=SignalDirection.LONG)), - CommandHandler('forceshort', partial( - self._force_enter, order_side=SignalDirection.SHORT)), - CommandHandler('reload_trade', self._reload_trade_from_exchange), - CommandHandler('trades', self._trades), - CommandHandler('delete', self._delete_trade), - CommandHandler(['coo', 'cancel_open_order'], self._cancel_open_order), - CommandHandler('performance', self._performance), - CommandHandler(['buys', 'entries'], self._enter_tag_performance), - CommandHandler(['sells', 'exits'], self._exit_reason_performance), - CommandHandler('mix_tags', self._mix_tag_performance), - CommandHandler('stats', self._stats), - CommandHandler('daily', self._daily), - CommandHandler('weekly', self._weekly), - CommandHandler('monthly', self._monthly), - CommandHandler('count', self._count), - CommandHandler('locks', self._locks), - CommandHandler(['unlock', 'delete_locks'], self._delete_locks), - CommandHandler(['reload_config', 'reload_conf'], self._reload_config), - CommandHandler(['show_config', 'show_conf'], self._show_config), - CommandHandler(['stopbuy', 'stopentry'], self._stopentry), - CommandHandler('whitelist', self._whitelist), - CommandHandler('blacklist', self._blacklist), - CommandHandler(['blacklist_delete', 'bl_delete'], self._blacklist_delete), - CommandHandler('logs', self._logs), - CommandHandler('edge', self._edge), - CommandHandler('health', self._health), - CommandHandler('help', self._help), - CommandHandler('version', self._version), - CommandHandler('marketdir', self._changemarketdir), - CommandHandler('order', self._order), - CommandHandler('list_custom_data', self._list_custom_data), + CommandHandler("status", self._status), + CommandHandler("profit", self._profit), + CommandHandler("balance", self._balance), + CommandHandler("start", self._start), + CommandHandler("stop", self._stop), + CommandHandler(["forcesell", "forceexit", "fx"], self._force_exit), + CommandHandler( + ["forcebuy", "forcelong"], + partial(self._force_enter, order_side=SignalDirection.LONG), + ), + CommandHandler( + "forceshort", partial(self._force_enter, order_side=SignalDirection.SHORT) + ), + CommandHandler("reload_trade", self._reload_trade_from_exchange), + CommandHandler("trades", self._trades), + CommandHandler("delete", self._delete_trade), + CommandHandler(["coo", "cancel_open_order"], self._cancel_open_order), + CommandHandler("performance", self._performance), + CommandHandler(["buys", "entries"], self._enter_tag_performance), + CommandHandler(["sells", "exits"], self._exit_reason_performance), + CommandHandler("mix_tags", self._mix_tag_performance), + CommandHandler("stats", self._stats), + CommandHandler("daily", self._daily), + CommandHandler("weekly", self._weekly), + CommandHandler("monthly", self._monthly), + CommandHandler("count", self._count), + CommandHandler("locks", self._locks), + CommandHandler(["unlock", "delete_locks"], self._delete_locks), + CommandHandler(["reload_config", "reload_conf"], self._reload_config), + CommandHandler(["show_config", "show_conf"], self._show_config), + CommandHandler(["stopbuy", "stopentry"], self._stopentry), + CommandHandler("whitelist", self._whitelist), + CommandHandler("blacklist", self._blacklist), + CommandHandler(["blacklist_delete", "bl_delete"], self._blacklist_delete), + CommandHandler("logs", self._logs), + CommandHandler("edge", self._edge), + CommandHandler("health", self._health), + CommandHandler("help", self._help), + CommandHandler("version", self._version), + CommandHandler("marketdir", self._changemarketdir), + CommandHandler("order", self._order), + CommandHandler("list_custom_data", self._list_custom_data), ] callbacks = [ - CallbackQueryHandler(self._status_table, pattern='update_status_table'), - CallbackQueryHandler(self._daily, pattern='update_daily'), - CallbackQueryHandler(self._weekly, pattern='update_weekly'), - CallbackQueryHandler(self._monthly, pattern='update_monthly'), - CallbackQueryHandler(self._profit, pattern='update_profit'), - CallbackQueryHandler(self._balance, pattern='update_balance'), - CallbackQueryHandler(self._performance, pattern='update_performance'), - CallbackQueryHandler(self._enter_tag_performance, - pattern='update_enter_tag_performance'), - CallbackQueryHandler(self._exit_reason_performance, - pattern='update_exit_reason_performance'), - CallbackQueryHandler(self._mix_tag_performance, pattern='update_mix_tag_performance'), - CallbackQueryHandler(self._count, pattern='update_count'), + CallbackQueryHandler(self._status_table, pattern="update_status_table"), + CallbackQueryHandler(self._daily, pattern="update_daily"), + CallbackQueryHandler(self._weekly, pattern="update_weekly"), + CallbackQueryHandler(self._monthly, pattern="update_monthly"), + CallbackQueryHandler(self._profit, pattern="update_profit"), + CallbackQueryHandler(self._balance, pattern="update_balance"), + CallbackQueryHandler(self._performance, pattern="update_performance"), + CallbackQueryHandler( + self._enter_tag_performance, pattern="update_enter_tag_performance" + ), + CallbackQueryHandler( + self._exit_reason_performance, pattern="update_exit_reason_performance" + ), + CallbackQueryHandler(self._mix_tag_performance, pattern="update_mix_tag_performance"), + CallbackQueryHandler(self._count, pattern="update_count"), CallbackQueryHandler(self._force_exit_inline, pattern=r"force_exit__\S+"), CallbackQueryHandler(self._force_enter_inline, pattern=r"force_enter__\S+"), ] @@ -275,8 +310,8 @@ class Telegram(RPCHandler): self._app.add_handler(callback) logger.info( - 'rpc.telegram is listening for following commands: %s', - [[x for x in sorted(h.commands)] for h in handles] + "rpc.telegram is listening for following commands: %s", + [[x for x in sorted(h.commands)] for h in handles], ) self._loop.run_until_complete(self._startup_telegram()) @@ -320,12 +355,14 @@ class Telegram(RPCHandler): return f"{msg['exchange']}{' (dry)' if self._config['dry_run'] else ''}" def _add_analyzed_candle(self, pair: str) -> str: - candle_val = self._config['telegram'].get( - 'notification_settings', {}).get('show_candle', 'off') - if candle_val != 'off': - if candle_val == 'ohlc': + candle_val = ( + self._config["telegram"].get("notification_settings", {}).get("show_candle", "off") + ) + if candle_val != "off": + if candle_val == "ohlc": analyzed_df, _ = self._rpc._freqtrade.dataprovider.get_analyzed_dataframe( - pair, self._config['timeframe']) + pair, self._config["timeframe"] + ) candle = analyzed_df.iloc[-1].squeeze() if len(analyzed_df) > 0 else None if candle is not None: return ( @@ -333,18 +370,17 @@ class Telegram(RPCHandler): f"{candle['low']}, {candle['close']}`\n" ) - return '' + return "" def _format_entry_msg(self, msg: RPCEntryMsg) -> str: - - is_fill = msg['type'] in [RPCMessageType.ENTRY_FILL] - emoji = '\N{CHECK MARK}' if is_fill else '\N{LARGE BLUE CIRCLE}' + is_fill = msg["type"] in [RPCMessageType.ENTRY_FILL] + emoji = "\N{CHECK MARK}" if is_fill else "\N{LARGE BLUE CIRCLE}" terminology = { - '1_enter': 'New Trade', - '1_entered': 'New Trade filled', - 'x_enter': 'Increasing position', - 'x_entered': 'Position increase filled', + "1_enter": "New Trade", + "1_entered": "New Trade filled", + "x_enter": "Increasing position", + "x_entered": "Position increase filled", } key = f"{'x' if msg['sub_trade'] else '1'}_{'entered' if is_fill else 'enter'}" @@ -355,65 +391,69 @@ class Telegram(RPCHandler): f" {wording} (#{msg['trade_id']})\n" f"*Pair:* `{msg['pair']}`\n" ) - message += self._add_analyzed_candle(msg['pair']) - message += f"*Enter Tag:* `{msg['enter_tag']}`\n" if msg.get('enter_tag') else "" + message += self._add_analyzed_candle(msg["pair"]) + message += f"*Enter Tag:* `{msg['enter_tag']}`\n" if msg.get("enter_tag") else "" message += f"*Amount:* `{round_value(msg['amount'], 8)}`\n" message += f"*Direction:* `{msg['direction']}" - if msg.get('leverage') and msg.get('leverage', 1.0) != 1.0: + if msg.get("leverage") and msg.get("leverage", 1.0) != 1.0: message += f" ({msg['leverage']:.3g}x)" message += "`\n" message += f"*Open Rate:* `{round_value(msg['open_rate'], 8)} {msg['quote_currency']}`\n" - if msg['type'] == RPCMessageType.ENTRY and msg['current_rate']: + if msg["type"] == RPCMessageType.ENTRY and msg["current_rate"]: message += ( f"*Current Rate:* `{round_value(msg['current_rate'], 8)} {msg['quote_currency']}`\n" ) - profit_fiat_extra = self.__format_profit_fiat(msg, 'stake_amount') # type: ignore - total = fmt_coin(msg['stake_amount'], msg['quote_currency']) + profit_fiat_extra = self.__format_profit_fiat(msg, "stake_amount") # type: ignore + total = fmt_coin(msg["stake_amount"], msg["quote_currency"]) message += f"*{'New ' if msg['sub_trade'] else ''}Total:* `{total}{profit_fiat_extra}`" return message def _format_exit_msg(self, msg: RPCExitMsg) -> str: - duration = msg['close_date'].replace( - microsecond=0) - msg['open_date'].replace(microsecond=0) + duration = msg["close_date"].replace(microsecond=0) - msg["open_date"].replace( + microsecond=0 + ) duration_min = duration.total_seconds() / 60 - leverage_text = (f" ({msg['leverage']:.3g}x)" - if msg.get('leverage') and msg.get('leverage', 1.0) != 1.0 - else "") + leverage_text = ( + f" ({msg['leverage']:.3g}x)" + if msg.get("leverage") and msg.get("leverage", 1.0) != 1.0 + else "" + ) - profit_fiat_extra = self.__format_profit_fiat(msg, 'profit_amount') + profit_fiat_extra = self.__format_profit_fiat(msg, "profit_amount") profit_extra = ( f" ({msg['gain']}: {fmt_coin(msg['profit_amount'], msg['quote_currency'])}" - f"{profit_fiat_extra})") + f"{profit_fiat_extra})" + ) - is_fill = msg['type'] == RPCMessageType.EXIT_FILL - is_sub_trade = msg.get('sub_trade') - is_sub_profit = msg['profit_amount'] != msg.get('cumulative_profit') - is_final_exit = msg.get('is_final_exit', False) and is_sub_profit - profit_prefix = 'Sub ' if is_sub_trade else '' - cp_extra = '' - exit_wording = 'Exited' if is_fill else 'Exiting' + is_fill = msg["type"] == RPCMessageType.EXIT_FILL + is_sub_trade = msg.get("sub_trade") + is_sub_profit = msg["profit_amount"] != msg.get("cumulative_profit") + is_final_exit = msg.get("is_final_exit", False) and is_sub_profit + profit_prefix = "Sub " if is_sub_trade else "" + cp_extra = "" + exit_wording = "Exited" if is_fill else "Exiting" if is_sub_trade or is_final_exit: - cp_fiat = self.__format_profit_fiat(msg, 'cumulative_profit') + cp_fiat = self.__format_profit_fiat(msg, "cumulative_profit") if is_final_exit: - profit_prefix = 'Sub ' + profit_prefix = "Sub " cp_extra = ( f"*Final Profit:* `{msg['final_profit_ratio']:.2%} " f"({msg['cumulative_profit']:.8f} {msg['quote_currency']}{cp_fiat})`\n" ) else: exit_wording = f"Partially {exit_wording.lower()}" - if msg['cumulative_profit']: + if msg["cumulative_profit"]: cp_extra = ( f"*Cumulative Profit:* `" f"{fmt_coin(msg['cumulative_profit'], msg['stake_currency'])}{cp_fiat}`\n" ) - enter_tag = f"*Enter Tag:* `{msg['enter_tag']}`\n" if msg.get('enter_tag') else "" + enter_tag = f"*Enter Tag:* `{msg['enter_tag']}`\n" if msg.get("enter_tag") else "" message = ( f"{self._get_exit_emoji(msg)} *{self._exchange_from_msg(msg)}:* " f"{exit_wording} {msg['pair']} (#{msg['trade_id']})\n" @@ -428,104 +468,108 @@ class Telegram(RPCHandler): f"*Amount:* `{round_value(msg['amount'], 8)}`\n" f"*Open Rate:* `{fmt_coin(msg['open_rate'], msg['quote_currency'])}`\n" ) - if msg['type'] == RPCMessageType.EXIT and msg['current_rate']: + if msg["type"] == RPCMessageType.EXIT and msg["current_rate"]: message += f"*Current Rate:* `{fmt_coin(msg['current_rate'], msg['quote_currency'])}`\n" - if msg['order_rate']: + if msg["order_rate"]: message += f"*Exit Rate:* `{fmt_coin(msg['order_rate'], msg['quote_currency'])}`" - elif msg['type'] == RPCMessageType.EXIT_FILL: + elif msg["type"] == RPCMessageType.EXIT_FILL: message += f"*Exit Rate:* `{fmt_coin(msg['close_rate'], msg['quote_currency'])}`" if is_sub_trade: - stake_amount_fiat = self.__format_profit_fiat(msg, 'stake_amount') + stake_amount_fiat = self.__format_profit_fiat(msg, "stake_amount") - rem = fmt_coin(msg['stake_amount'], msg['quote_currency']) + rem = fmt_coin(msg["stake_amount"], msg["quote_currency"]) message += f"\n*Remaining:* `{rem}{stake_amount_fiat}`" else: message += f"\n*Duration:* `{duration} ({duration_min:.1f} min)`" return message def __format_profit_fiat( - self, - msg: RPCExitMsg, - key: Literal['stake_amount', 'profit_amount', 'cumulative_profit'] + self, msg: RPCExitMsg, key: Literal["stake_amount", "profit_amount", "cumulative_profit"] ) -> str: """ Format Fiat currency to append to regular profit output """ - profit_fiat_extra = '' - if self._rpc._fiat_converter and (fiat_currency := msg.get('fiat_currency')): + profit_fiat_extra = "" + if self._rpc._fiat_converter and (fiat_currency := msg.get("fiat_currency")): profit_fiat = self._rpc._fiat_converter.convert_amount( - msg[key], msg['stake_currency'], fiat_currency) + msg[key], msg["stake_currency"], fiat_currency + ) profit_fiat_extra = f" / {profit_fiat:.3f} {fiat_currency}" return profit_fiat_extra def compose_message(self, msg: RPCSendMsg) -> Optional[str]: - if msg['type'] == RPCMessageType.ENTRY or msg['type'] == RPCMessageType.ENTRY_FILL: + if msg["type"] == RPCMessageType.ENTRY or msg["type"] == RPCMessageType.ENTRY_FILL: message = self._format_entry_msg(msg) - elif msg['type'] == RPCMessageType.EXIT or msg['type'] == RPCMessageType.EXIT_FILL: + elif msg["type"] == RPCMessageType.EXIT or msg["type"] == RPCMessageType.EXIT_FILL: message = self._format_exit_msg(msg) elif ( - msg['type'] == RPCMessageType.ENTRY_CANCEL - or msg['type'] == RPCMessageType.EXIT_CANCEL + msg["type"] == RPCMessageType.ENTRY_CANCEL or msg["type"] == RPCMessageType.EXIT_CANCEL ): - message_side = 'enter' if msg['type'] == RPCMessageType.ENTRY_CANCEL else 'exit' - message = (f"\N{WARNING SIGN} *{self._exchange_from_msg(msg)}:* " - f"Cancelling {'partial ' if msg.get('sub_trade') else ''}" - f"{message_side} Order for {msg['pair']} " - f"(#{msg['trade_id']}). Reason: {msg['reason']}.") + message_side = "enter" if msg["type"] == RPCMessageType.ENTRY_CANCEL else "exit" + message = ( + f"\N{WARNING SIGN} *{self._exchange_from_msg(msg)}:* " + f"Cancelling {'partial ' if msg.get('sub_trade') else ''}" + f"{message_side} Order for {msg['pair']} " + f"(#{msg['trade_id']}). Reason: {msg['reason']}." + ) - elif msg['type'] == RPCMessageType.PROTECTION_TRIGGER: + elif msg["type"] == RPCMessageType.PROTECTION_TRIGGER: message = ( f"*Protection* triggered due to {msg['reason']}. " f"`{msg['pair']}` will be locked until `{msg['lock_end_time']}`." ) - elif msg['type'] == RPCMessageType.PROTECTION_TRIGGER_GLOBAL: + elif msg["type"] == RPCMessageType.PROTECTION_TRIGGER_GLOBAL: message = ( f"*Protection* triggered due to {msg['reason']}. " f"*All pairs* will be locked until `{msg['lock_end_time']}`." ) - elif msg['type'] == RPCMessageType.STATUS: + elif msg["type"] == RPCMessageType.STATUS: message = f"*Status:* `{msg['status']}`" - elif msg['type'] == RPCMessageType.WARNING: + elif msg["type"] == RPCMessageType.WARNING: message = f"\N{WARNING SIGN} *Warning:* `{msg['status']}`" - elif msg['type'] == RPCMessageType.EXCEPTION: + elif msg["type"] == RPCMessageType.EXCEPTION: # Errors will contain exceptions, which are wrapped in triple ticks. message = f"\N{WARNING SIGN} *ERROR:* \n {msg['status']}" - elif msg['type'] == RPCMessageType.STARTUP: + elif msg["type"] == RPCMessageType.STARTUP: message = f"{msg['status']}" - elif msg['type'] == RPCMessageType.STRATEGY_MSG: + elif msg["type"] == RPCMessageType.STRATEGY_MSG: message = f"{msg['msg']}" else: - logger.debug("Unknown message type: %s", msg['type']) + logger.debug("Unknown message type: %s", msg["type"]) return None return message def send_msg(self, msg: RPCSendMsg) -> None: - """ Send a message to telegram channel """ + """Send a message to telegram channel""" - default_noti = 'on' + default_noti = "on" - msg_type = msg['type'] - noti = '' - if msg['type'] == RPCMessageType.EXIT: - sell_noti = self._config['telegram'] \ - .get('notification_settings', {}).get(str(msg_type), {}) + msg_type = msg["type"] + noti = "" + if msg["type"] == RPCMessageType.EXIT: + sell_noti = ( + self._config["telegram"].get("notification_settings", {}).get(str(msg_type), {}) + ) # For backward compatibility sell still can be string if isinstance(sell_noti, str): noti = sell_noti else: - noti = sell_noti.get(str(msg['exit_reason']), default_noti) + noti = sell_noti.get(str(msg["exit_reason"]), default_noti) else: - noti = self._config['telegram'] \ - .get('notification_settings', {}).get(str(msg_type), default_noti) + noti = ( + self._config["telegram"] + .get("notification_settings", {}) + .get(str(msg_type), default_noti) + ) - if noti == 'off': + if noti == "off": logger.info(f"Notification '{msg_type}' not sent.") # Notification disabled return @@ -533,19 +577,19 @@ class Telegram(RPCHandler): message = self.compose_message(deepcopy(msg)) if message: asyncio.run_coroutine_threadsafe( - self._send_msg(message, disable_notification=(noti == 'silent')), - self._loop) + self._send_msg(message, disable_notification=(noti == "silent")), self._loop + ) def _get_exit_emoji(self, msg): """ Get emoji for exit-messages """ - if float(msg['profit_ratio']) >= 0.05: + if float(msg["profit_ratio"]) >= 0.05: return "\N{ROCKET}" - elif float(msg['profit_ratio']) >= 0.0: + elif float(msg["profit_ratio"]) >= 0.0: return "\N{EIGHT SPOKED ASTERISK}" - elif msg['exit_reason'] == "stop_loss": + elif msg["exit_reason"] == "stop_loss": return "\N{WARNING SIGN}" else: return "\N{CROSS MARK}" @@ -560,10 +604,10 @@ class Telegram(RPCHandler): order_nr = 0 for order in filled_orders: lines: List[str] = [] - if order['is_open'] is True: + if order["is_open"] is True: continue order_nr += 1 - wording = 'Entry' if order['ft_is_entry'] else 'Exit' + wording = "Entry" if order["ft_is_entry"] else "Exit" cur_entry_amount = order["filled"] or order["amount"] cur_entry_average = order["safe_price"] @@ -577,13 +621,17 @@ class Telegram(RPCHandler): lines.append(f"*Average Price:* {round_value(cur_entry_average, 8)}") else: # TODO: This calculation ignores fees. - price_to_1st_entry = ((cur_entry_average - first_avg) / first_avg) + price_to_1st_entry = (cur_entry_average - first_avg) / first_avg if is_open: lines.append("({})".format(dt_humanize_delta(order["order_filled_date"]))) - lines.append(f"*Amount:* {round_value(cur_entry_amount, 8)} " - f"({fmt_coin(order['cost'], quote_currency)})") - lines.append(f"*Average {wording} Price:* {round_value(cur_entry_average, 8)} " - f"({price_to_1st_entry:.2%} from 1st entry rate)") + lines.append( + f"*Amount:* {round_value(cur_entry_amount, 8)} " + f"({fmt_coin(order['cost'], quote_currency)})" + ) + lines.append( + f"*Average {wording} Price:* {round_value(cur_entry_average, 8)} " + f"({price_to_1st_entry:.2%} from 1st entry rate)" + ) lines.append(f"*Order Filled:* {order['order_filled_date']}") lines_detail.append("\n".join(lines)) @@ -606,12 +654,11 @@ class Telegram(RPCHandler): results = self._rpc._rpc_trade_status(trade_ids=trade_ids) for r in results: - lines = [ - "*Order List for Trade #*`{trade_id}`" - ] + lines = ["*Order List for Trade #*`{trade_id}`"] lines_detail = self._prepare_order_details( - r['orders'], r['quote_currency'], r['is_open']) + r["orders"], r["quote_currency"], r["is_open"] + ) lines.extend(lines_detail if lines_detail else "") await self.__send_order_msg(lines, r) @@ -619,15 +666,15 @@ class Telegram(RPCHandler): """ Send status message. """ - msg = '' + msg = "" for line in lines: if line: if (len(msg) + len(line) + 1) < MAX_MESSAGE_LENGTH: - msg += line + '\n' + msg += line + "\n" else: await self._send_msg(msg.format(**r)) - msg = "*Order List for Trade #*`{trade_id}` - continued\n" + line + '\n' + msg = "*Order List for Trade #*`{trade_id}` - continued\n" + line + "\n" await self._send_msg(msg.format(**r)) @@ -641,7 +688,7 @@ class Telegram(RPCHandler): :return: None """ - if context.args and 'table' in context.args: + if context.args and "table" in context.args: await self._status_table(update, context) return else: @@ -659,74 +706,96 @@ class Telegram(RPCHandler): trade_ids = [int(i) for i in context.args if i.isnumeric()] results = self._rpc._rpc_trade_status(trade_ids=trade_ids) - position_adjust = self._config.get('position_adjustment_enable', False) - max_entries = self._config.get('max_entry_position_adjustment', -1) + position_adjust = self._config.get("position_adjustment_enable", False) + max_entries = self._config.get("max_entry_position_adjustment", -1) for r in results: - r['open_date_hum'] = dt_humanize_delta(r['open_date']) - r['num_entries'] = len([o for o in r['orders'] if o['ft_is_entry']]) - r['num_exits'] = len([o for o in r['orders'] if not o['ft_is_entry'] - and not o['ft_order_side'] == 'stoploss']) - r['exit_reason'] = r.get('exit_reason', "") - r['stake_amount_r'] = fmt_coin(r['stake_amount'], r['quote_currency']) - r['max_stake_amount_r'] = fmt_coin( - r['max_stake_amount'] or r['stake_amount'], r['quote_currency']) - r['profit_abs_r'] = fmt_coin(r['profit_abs'], r['quote_currency']) - r['realized_profit_r'] = fmt_coin(r['realized_profit'], r['quote_currency']) - r['total_profit_abs_r'] = fmt_coin( - r['total_profit_abs'], r['quote_currency']) + r["open_date_hum"] = dt_humanize_delta(r["open_date"]) + r["num_entries"] = len([o for o in r["orders"] if o["ft_is_entry"]]) + r["num_exits"] = len( + [ + o + for o in r["orders"] + if not o["ft_is_entry"] and not o["ft_order_side"] == "stoploss" + ] + ) + r["exit_reason"] = r.get("exit_reason", "") + r["stake_amount_r"] = fmt_coin(r["stake_amount"], r["quote_currency"]) + r["max_stake_amount_r"] = fmt_coin( + r["max_stake_amount"] or r["stake_amount"], r["quote_currency"] + ) + r["profit_abs_r"] = fmt_coin(r["profit_abs"], r["quote_currency"]) + r["realized_profit_r"] = fmt_coin(r["realized_profit"], r["quote_currency"]) + r["total_profit_abs_r"] = fmt_coin(r["total_profit_abs"], r["quote_currency"]) lines = [ - "*Trade ID:* `{trade_id}`" + - (" `(since {open_date_hum})`" if r['is_open'] else ""), + "*Trade ID:* `{trade_id}`" + (" `(since {open_date_hum})`" if r["is_open"] else ""), "*Current Pair:* {pair}", - f"*Direction:* {'`Short`' if r.get('is_short') else '`Long`'}" - + " ` ({leverage}x)`" if r.get('leverage') else "", + f"*Direction:* {'`Short`' if r.get('is_short') else '`Long`'}" + " ` ({leverage}x)`" + if r.get("leverage") + else "", "*Amount:* `{amount} ({stake_amount_r})`", "*Total invested:* `{max_stake_amount_r}`" if position_adjust else "", - "*Enter Tag:* `{enter_tag}`" if r['enter_tag'] else "", - "*Exit Reason:* `{exit_reason}`" if r['exit_reason'] else "", + "*Enter Tag:* `{enter_tag}`" if r["enter_tag"] else "", + "*Exit Reason:* `{exit_reason}`" if r["exit_reason"] else "", ] if position_adjust: - max_buy_str = (f"/{max_entries + 1}" if (max_entries > 0) else "") - lines.extend([ - "*Number of Entries:* `{num_entries}" + max_buy_str + "`", - "*Number of Exits:* `{num_exits}`" - ]) + max_buy_str = f"/{max_entries + 1}" if (max_entries > 0) else "" + lines.extend( + [ + "*Number of Entries:* `{num_entries}" + max_buy_str + "`", + "*Number of Exits:* `{num_exits}`", + ] + ) - lines.extend([ - f"*Open Rate:* `{round_value(r['open_rate'], 8)}`", - f"*Close Rate:* `{round_value(r['close_rate'], 8)}`" if r['close_rate'] else "", - "*Open Date:* `{open_date}`", - "*Close Date:* `{close_date}`" if r['close_date'] else "", - f" \n*Current Rate:* `{round_value(r['current_rate'], 8)}`" if r['is_open'] else "", - ("*Unrealized Profit:* " if r['is_open'] else "*Close Profit: *") - + "`{profit_ratio:.2%}` `({profit_abs_r})`", - ]) + lines.extend( + [ + f"*Open Rate:* `{round_value(r['open_rate'], 8)}`", + f"*Close Rate:* `{round_value(r['close_rate'], 8)}`" if r["close_rate"] else "", + "*Open Date:* `{open_date}`", + "*Close Date:* `{close_date}`" if r["close_date"] else "", + f" \n*Current Rate:* `{round_value(r['current_rate'], 8)}`" + if r["is_open"] + else "", + ("*Unrealized Profit:* " if r["is_open"] else "*Close Profit: *") + + "`{profit_ratio:.2%}` `({profit_abs_r})`", + ] + ) - if r['is_open']: - if r.get('realized_profit'): - lines.extend([ - "*Realized Profit:* `{realized_profit_ratio:.2%} ({realized_profit_r})`", - "*Total Profit:* `{total_profit_ratio:.2%} ({total_profit_abs_r})`" - ]) + if r["is_open"]: + if r.get("realized_profit"): + lines.extend( + [ + "*Realized Profit:* `{realized_profit_ratio:.2%} ({realized_profit_r})`", + "*Total Profit:* `{total_profit_ratio:.2%} ({total_profit_abs_r})`", + ] + ) # Append empty line to improve readability lines.append(" ") - if (r['stop_loss_abs'] != r['initial_stop_loss_abs'] - and r['initial_stop_loss_ratio'] is not None): + if ( + r["stop_loss_abs"] != r["initial_stop_loss_abs"] + and r["initial_stop_loss_ratio"] is not None + ): # Adding initial stoploss only if it is different from stoploss - lines.append("*Initial Stoploss:* `{initial_stop_loss_abs:.8f}` " - "`({initial_stop_loss_ratio:.2%})`") + lines.append( + "*Initial Stoploss:* `{initial_stop_loss_abs:.8f}` " + "`({initial_stop_loss_ratio:.2%})`" + ) # Adding stoploss and stoploss percentage only if it is not None - lines.append(f"*Stoploss:* `{round_value(r['stop_loss_abs'], 8)}` " + - ("`({stop_loss_ratio:.2%})`" if r['stop_loss_ratio'] else "")) - lines.append(f"*Stoploss distance:* `{round_value(r['stoploss_current_dist'], 8)}` " - "`({stoploss_current_dist_ratio:.2%})`") - if r.get('open_orders'): + lines.append( + f"*Stoploss:* `{round_value(r['stop_loss_abs'], 8)}` " + + ("`({stop_loss_ratio:.2%})`" if r["stop_loss_ratio"] else "") + ) + lines.append( + f"*Stoploss distance:* `{round_value(r['stoploss_current_dist'], 8)}` " + "`({stoploss_current_dist_ratio:.2%})`" + ) + if r.get("open_orders"): lines.append( "*Open Order:* `{open_orders}`" - + ("- `{exit_order_status}`" if r['exit_order_status'] else "")) + + ("- `{exit_order_status}`" if r["exit_order_status"] else "") + ) await self.__send_status_msg(lines, r) @@ -734,15 +803,15 @@ class Telegram(RPCHandler): """ Send status message. """ - msg = '' + msg = "" for line in lines: if line: if (len(msg) + len(line) + 1) < MAX_MESSAGE_LENGTH: - msg += line + '\n' + msg += line + "\n" else: await self._send_msg(msg.format(**r)) - msg = "*Trade ID:* `{trade_id}` - continued\n" + line + '\n' + msg = "*Trade ID:* `{trade_id}` - continued\n" + line + "\n" await self._send_msg(msg.format(**r)) @@ -755,9 +824,10 @@ class Telegram(RPCHandler): :param update: message update :return: None """ - fiat_currency = self._config.get('fiat_display_currency', '') + fiat_currency = self._config.get("fiat_display_currency", "") statlist, head, fiat_profit_sum = self._rpc._rpc_status_table( - self._config['stake_currency'], fiat_currency) + self._config["stake_currency"], fiat_currency + ) show_total = not isnan(fiat_profit_sum) and len(statlist) > 1 max_trades_per_msg = 50 @@ -768,21 +838,23 @@ class Telegram(RPCHandler): """ messages_count = max(int(len(statlist) / max_trades_per_msg + 0.99), 1) for i in range(0, messages_count): - trades = statlist[i * max_trades_per_msg:(i + 1) * max_trades_per_msg] + trades = statlist[i * max_trades_per_msg : (i + 1) * max_trades_per_msg] if show_total and i == messages_count - 1: # append total line trades.append(["Total", "", "", f"{fiat_profit_sum:.2f} {fiat_currency}"]) - message = tabulate(trades, - headers=head, - tablefmt='simple') + message = tabulate(trades, headers=head, tablefmt="simple") if show_total and i == messages_count - 1: # insert separators line between Total lines = message.split("\n") message = "\n".join(lines[:-1] + [lines[1]] + [lines[-1]]) - await self._send_msg(f"
{message}
", parse_mode=ParseMode.HTML, - reload_able=True, callback_path="update_status_table", - query=update.callback_query) + await self._send_msg( + f"
{message}
", + parse_mode=ParseMode.HTML, + reload_able=True, + callback_path="update_status_table", + query=update.callback_query, + ) async def _timeunit_stats(self, update: Update, context: CallbackContext, unit: str) -> None: """ @@ -794,45 +866,51 @@ class Telegram(RPCHandler): """ vals = { - 'days': TimeunitMappings('Day', 'Daily', 'days', 'update_daily', 7, '%Y-%m-%d'), - 'weeks': TimeunitMappings('Monday', 'Weekly', 'weeks (starting from Monday)', - 'update_weekly', 8, '%Y-%m-%d'), - 'months': TimeunitMappings('Month', 'Monthly', 'months', 'update_monthly', 6, '%Y-%m'), + "days": TimeunitMappings("Day", "Daily", "days", "update_daily", 7, "%Y-%m-%d"), + "weeks": TimeunitMappings( + "Monday", "Weekly", "weeks (starting from Monday)", "update_weekly", 8, "%Y-%m-%d" + ), + "months": TimeunitMappings("Month", "Monthly", "months", "update_monthly", 6, "%Y-%m"), } val = vals[unit] - stake_cur = self._config['stake_currency'] - fiat_disp_cur = self._config.get('fiat_display_currency', '') + stake_cur = self._config["stake_currency"] + fiat_disp_cur = self._config.get("fiat_display_currency", "") try: timescale = int(context.args[0]) if context.args else val.default except (TypeError, ValueError, IndexError): timescale = val.default - stats = self._rpc._rpc_timeunit_profit( - timescale, - stake_cur, - fiat_disp_cur, - unit - ) + stats = self._rpc._rpc_timeunit_profit(timescale, stake_cur, fiat_disp_cur, unit) stats_tab = tabulate( - [[f"{period['date']:{val.dateformat}} ({period['trade_count']})", - f"{fmt_coin(period['abs_profit'], stats['stake_currency'])}", - f"{period['fiat_value']:.2f} {stats['fiat_display_currency']}", - f"{period['rel_profit']:.2%}", - ] for period in stats['data']], + [ + [ + f"{period['date']:{val.dateformat}} ({period['trade_count']})", + f"{fmt_coin(period['abs_profit'], stats['stake_currency'])}", + f"{period['fiat_value']:.2f} {stats['fiat_display_currency']}", + f"{period['rel_profit']:.2%}", + ] + for period in stats["data"] + ], headers=[ f"{val.header} (count)", - f'{stake_cur}', - f'{fiat_disp_cur}', - 'Profit %', - 'Trades', + f"{stake_cur}", + f"{fiat_disp_cur}", + "Profit %", + "Trades", ], - tablefmt='simple') - message = ( - f'{val.message} Profit over the last {timescale} {val.message2}:\n' - f'
{stats_tab}
' + tablefmt="simple", + ) + message = ( + f"{val.message} Profit over the last {timescale} {val.message2}:\n" + f"
{stats_tab}
" + ) + await self._send_msg( + message, + parse_mode=ParseMode.HTML, + reload_able=True, + callback_path=val.callback, + query=update.callback_query, ) - await self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True, - callback_path=val.callback, query=update.callback_query) @authorized_only async def _daily(self, update: Update, context: CallbackContext) -> None: @@ -843,7 +921,7 @@ class Telegram(RPCHandler): :param update: message update :return: None """ - await self._timeunit_stats(update, context, 'days') + await self._timeunit_stats(update, context, "days") @authorized_only async def _weekly(self, update: Update, context: CallbackContext) -> None: @@ -854,7 +932,7 @@ class Telegram(RPCHandler): :param update: message update :return: None """ - await self._timeunit_stats(update, context, 'weeks') + await self._timeunit_stats(update, context, "weeks") @authorized_only async def _monthly(self, update: Update, context: CallbackContext) -> None: @@ -865,7 +943,7 @@ class Telegram(RPCHandler): :param update: message update :return: None """ - await self._timeunit_stats(update, context, 'months') + await self._timeunit_stats(update, context, "months") @authorized_only async def _profit(self, update: Update, context: CallbackContext) -> None: @@ -876,8 +954,8 @@ class Telegram(RPCHandler): :param update: message update :return: None """ - stake_cur = self._config['stake_currency'] - fiat_disp_cur = self._config.get('fiat_display_currency', '') + stake_cur = self._config["stake_currency"] + fiat_disp_cur = self._config.get("fiat_display_currency", "") start_date = datetime.fromtimestamp(0) timescale = None @@ -889,38 +967,37 @@ class Telegram(RPCHandler): except (TypeError, ValueError, IndexError): pass - stats = self._rpc._rpc_trade_statistics( - stake_cur, - fiat_disp_cur, - start_date) - profit_closed_coin = stats['profit_closed_coin'] - profit_closed_ratio_mean = stats['profit_closed_ratio_mean'] - profit_closed_percent = stats['profit_closed_percent'] - profit_closed_fiat = stats['profit_closed_fiat'] - profit_all_coin = stats['profit_all_coin'] - profit_all_ratio_mean = stats['profit_all_ratio_mean'] - profit_all_percent = stats['profit_all_percent'] - profit_all_fiat = stats['profit_all_fiat'] - trade_count = stats['trade_count'] + stats = self._rpc._rpc_trade_statistics(stake_cur, fiat_disp_cur, start_date) + profit_closed_coin = stats["profit_closed_coin"] + profit_closed_ratio_mean = stats["profit_closed_ratio_mean"] + profit_closed_percent = stats["profit_closed_percent"] + profit_closed_fiat = stats["profit_closed_fiat"] + profit_all_coin = stats["profit_all_coin"] + profit_all_ratio_mean = stats["profit_all_ratio_mean"] + profit_all_percent = stats["profit_all_percent"] + profit_all_fiat = stats["profit_all_fiat"] + trade_count = stats["trade_count"] first_trade_date = f"{stats['first_trade_humanized']} ({stats['first_trade_date']})" latest_trade_date = f"{stats['latest_trade_humanized']} ({stats['latest_trade_date']})" - avg_duration = stats['avg_duration'] - best_pair = stats['best_pair'] - best_pair_profit_ratio = stats['best_pair_profit_ratio'] - winrate = stats['winrate'] - expectancy = stats['expectancy'] - expectancy_ratio = stats['expectancy_ratio'] + avg_duration = stats["avg_duration"] + best_pair = stats["best_pair"] + best_pair_profit_ratio = stats["best_pair_profit_ratio"] + winrate = stats["winrate"] + expectancy = stats["expectancy"] + expectancy_ratio = stats["expectancy_ratio"] - if stats['trade_count'] == 0: + if stats["trade_count"] == 0: markdown_msg = f"No trades yet.\n*Bot started:* `{stats['bot_start_date']}`" else: # Message to display - if stats['closed_trade_count'] > 0: - markdown_msg = ("*ROI:* Closed trades\n" - f"∙ `{fmt_coin(profit_closed_coin, stake_cur)} " - f"({profit_closed_ratio_mean:.2%}) " - f"({profit_closed_percent} \N{GREEK CAPITAL LETTER SIGMA}%)`\n" - f"∙ `{fmt_coin(profit_closed_fiat, fiat_disp_cur)}`\n") + if stats["closed_trade_count"] > 0: + markdown_msg = ( + "*ROI:* Closed trades\n" + f"∙ `{fmt_coin(profit_closed_coin, stake_cur)} " + f"({profit_closed_ratio_mean:.2%}) " + f"({profit_closed_percent} \N{GREEK CAPITAL LETTER SIGMA}%)`\n" + f"∙ `{fmt_coin(profit_closed_fiat, fiat_disp_cur)}`\n" + ) else: markdown_msg = "`No closed trade` \n" fiat_all_trades = ( @@ -941,7 +1018,7 @@ class Telegram(RPCHandler): f"*Winrate:* `{winrate:.2%}`\n" f"*Expectancy (Ratio):* `{expectancy:.2f} ({expectancy_ratio:.2f})`" ) - if stats['closed_trade_count'] > 0: + if stats["closed_trade_count"] > 0: markdown_msg += ( f"\n*Avg. Duration:* `{avg_duration}`\n" f"*Best Performing:* `{best_pair}: {best_pair_profit_ratio:.2%}`\n" @@ -954,8 +1031,12 @@ class Telegram(RPCHandler): f" to `{stats['max_drawdown_end']} " f"({fmt_coin(stats['drawdown_low'], stake_cur)})`\n" ) - await self._send_msg(markdown_msg, reload_able=True, callback_path="update_profit", - query=update.callback_query) + await self._send_msg( + markdown_msg, + reload_able=True, + callback_path="update_profit", + query=update.callback_query, + ) @authorized_only async def _stats(self, update: Update, context: CallbackContext) -> None: @@ -966,86 +1047,90 @@ class Telegram(RPCHandler): stats = self._rpc._rpc_stats() reason_map = { - 'roi': 'ROI', - 'stop_loss': 'Stoploss', - 'trailing_stop_loss': 'Trail. Stop', - 'stoploss_on_exchange': 'Stoploss', - 'exit_signal': 'Exit Signal', - 'force_exit': 'Force Exit', - 'emergency_exit': 'Emergency Exit', + "roi": "ROI", + "stop_loss": "Stoploss", + "trailing_stop_loss": "Trail. Stop", + "stoploss_on_exchange": "Stoploss", + "exit_signal": "Exit Signal", + "force_exit": "Force Exit", + "emergency_exit": "Emergency Exit", } exit_reasons_tabulate = [ - [ - reason_map.get(reason, reason), - sum(count.values()), - count['wins'], - count['losses'] - ] for reason, count in stats['exit_reasons'].items() + [reason_map.get(reason, reason), sum(count.values()), count["wins"], count["losses"]] + for reason, count in stats["exit_reasons"].items() ] - exit_reasons_msg = 'No trades yet.' + exit_reasons_msg = "No trades yet." for reason in chunks(exit_reasons_tabulate, 25): - exit_reasons_msg = tabulate( - reason, - headers=['Exit Reason', 'Exits', 'Wins', 'Losses'] - ) + exit_reasons_msg = tabulate(reason, headers=["Exit Reason", "Exits", "Wins", "Losses"]) if len(exit_reasons_tabulate) > 25: await self._send_msg(f"```\n{exit_reasons_msg}```", ParseMode.MARKDOWN) - exit_reasons_msg = '' + exit_reasons_msg = "" - durations = stats['durations'] + durations = stats["durations"] duration_msg = tabulate( [ - ['Wins', str(timedelta(seconds=durations['wins'])) - if durations['wins'] is not None else 'N/A'], - ['Losses', str(timedelta(seconds=durations['losses'])) - if durations['losses'] is not None else 'N/A'] + [ + "Wins", + str(timedelta(seconds=durations["wins"])) + if durations["wins"] is not None + else "N/A", + ], + [ + "Losses", + str(timedelta(seconds=durations["losses"])) + if durations["losses"] is not None + else "N/A", + ], ], - headers=['', 'Avg. Duration'] + headers=["", "Avg. Duration"], ) - msg = (f"""```\n{exit_reasons_msg}```\n```\n{duration_msg}```""") + msg = f"""```\n{exit_reasons_msg}```\n```\n{duration_msg}```""" await self._send_msg(msg, ParseMode.MARKDOWN) @authorized_only async def _balance(self, update: Update, context: CallbackContext) -> None: - """ Handler for /balance """ - full_result = context.args and 'full' in context.args - result = self._rpc._rpc_balance(self._config['stake_currency'], - self._config.get('fiat_display_currency', '')) + """Handler for /balance""" + full_result = context.args and "full" in context.args + result = self._rpc._rpc_balance( + self._config["stake_currency"], self._config.get("fiat_display_currency", "") + ) - balance_dust_level = self._config['telegram'].get('balance_dust_level', 0.0) + balance_dust_level = self._config["telegram"].get("balance_dust_level", 0.0) if not balance_dust_level: - balance_dust_level = DUST_PER_COIN.get(self._config['stake_currency'], 1.0) + balance_dust_level = DUST_PER_COIN.get(self._config["stake_currency"], 1.0) - output = '' - if self._config['dry_run']: + output = "" + if self._config["dry_run"]: output += "*Warning:* Simulated balances in Dry Mode.\n" - starting_cap = fmt_coin(result['starting_capital'], self._config['stake_currency']) + starting_cap = fmt_coin(result["starting_capital"], self._config["stake_currency"]) output += f"Starting capital: `{starting_cap}`" - starting_cap_fiat = fmt_coin( - result['starting_capital_fiat'], self._config['fiat_display_currency'] - ) if result['starting_capital_fiat'] > 0 else '' - output += (f" `, {starting_cap_fiat}`.\n" - ) if result['starting_capital_fiat'] > 0 else '.\n' + starting_cap_fiat = ( + fmt_coin(result["starting_capital_fiat"], self._config["fiat_display_currency"]) + if result["starting_capital_fiat"] > 0 + else "" + ) + output += (f" `, {starting_cap_fiat}`.\n") if result["starting_capital_fiat"] > 0 else ".\n" total_dust_balance = 0 total_dust_currencies = 0 - for curr in result['currencies']: - curr_output = '' - if ( - (curr['is_position'] or curr['est_stake'] > balance_dust_level) - and (full_result or curr['is_bot_managed']) + for curr in result["currencies"]: + curr_output = "" + if (curr["is_position"] or curr["est_stake"] > balance_dust_level) and ( + full_result or curr["is_bot_managed"] ): - if curr['is_position']: + if curr["is_position"]: curr_output = ( f"*{curr['currency']}:*\n" f"\t`{curr['side']}: {curr['position']:.8f}`\n" f"\t`Leverage: {curr['leverage']:.1f}`\n" f"\t`Est. {curr['stake']}: " - f"{fmt_coin(curr['est_stake'], curr['stake'], False)}`\n") + f"{fmt_coin(curr['est_stake'], curr['stake'], False)}`\n" + ) else: est_stake = fmt_coin( - curr['est_stake' if full_result else 'est_stake_bot'], curr['stake'], False) + curr["est_stake" if full_result else "est_stake_bot"], curr["stake"], False + ) curr_output = ( f"*{curr['currency']}:*\n" @@ -1053,10 +1138,11 @@ class Telegram(RPCHandler): f"\t`Balance: {curr['balance']:.8f}`\n" f"\t`Pending: {curr['used']:.8f}`\n" f"\t`Bot Owned: {curr['bot_owned']:.8f}`\n" - f"\t`Est. {curr['stake']}: {est_stake}`\n") + f"\t`Est. {curr['stake']}: {est_stake}`\n" + ) - elif curr['est_stake'] <= balance_dust_level: - total_dust_balance += curr['est_stake'] + elif curr["est_stake"] <= balance_dust_level: + total_dust_balance += curr["est_stake"] total_dust_currencies += 1 # Handle overflowing message length @@ -1072,21 +1158,23 @@ class Telegram(RPCHandler): f"{plural(total_dust_currencies, 'Currency', 'Currencies')} " f"(< {balance_dust_level} {result['stake']}):*\n" f"\t`Est. {result['stake']}: " - f"{fmt_coin(total_dust_balance, result['stake'], False)}`\n") - tc = result['trade_count'] > 0 - stake_improve = f" `({result['starting_capital_ratio']:.2%})`" if tc else '' - fiat_val = f" `({result['starting_capital_fiat_ratio']:.2%})`" if tc else '' - value = fmt_coin( - result['value' if full_result else 'value_bot'], result['symbol'], False) + f"{fmt_coin(total_dust_balance, result['stake'], False)}`\n" + ) + tc = result["trade_count"] > 0 + stake_improve = f" `({result['starting_capital_ratio']:.2%})`" if tc else "" + fiat_val = f" `({result['starting_capital_fiat_ratio']:.2%})`" if tc else "" + value = fmt_coin(result["value" if full_result else "value_bot"], result["symbol"], False) total_stake = fmt_coin( - result['total' if full_result else 'total_bot'], result['stake'], False) + result["total" if full_result else "total_bot"], result["stake"], False + ) output += ( f"\n*Estimated Value{' (Bot managed assets only)' if not full_result else ''}*:\n" f"\t`{result['stake']}: {total_stake}`{stake_improve}\n" f"\t`{result['symbol']}: {value}`{fiat_val}\n" ) - await self._send_msg(output, reload_able=True, callback_path="update_balance", - query=update.callback_query) + await self._send_msg( + output, reload_able=True, callback_path="update_balance", query=update.callback_query + ) @authorized_only async def _start(self, update: Update, context: CallbackContext) -> None: @@ -1161,12 +1249,13 @@ class Telegram(RPCHandler): trade_id = context.args[0] await self._force_exit_action(trade_id) else: - fiat_currency = self._config.get('fiat_display_currency', '') + fiat_currency = self._config.get("fiat_display_currency", "") try: statlist, _, _ = self._rpc._rpc_status_table( - self._config['stake_currency'], fiat_currency) + self._config["stake_currency"], fiat_currency + ) except RPCException: - await self._send_msg(msg='No open trade found.') + await self._send_msg(msg="No open trade found.") return trades = [] for trade in statlist: @@ -1174,15 +1263,17 @@ class Telegram(RPCHandler): trade_buttons = [ InlineKeyboardButton(text=trade[1], callback_data=f"force_exit__{trade[0]}") - for trade in trades] + for trade in trades + ] buttons_aligned = self._layout_inline_keyboard_onecol(trade_buttons) - buttons_aligned.append([InlineKeyboardButton( - text='Cancel', callback_data='force_exit__cancel')]) + buttons_aligned.append( + [InlineKeyboardButton(text="Cancel", callback_data="force_exit__cancel")] + ) await self._send_msg(msg="Which trade?", keyboard=buttons_aligned) async def _force_exit_action(self, trade_id: str): - if trade_id != 'cancel': + if trade_id != "cancel": try: loop = asyncio.get_running_loop() # Workaround to avoid nested loops @@ -1193,10 +1284,10 @@ class Telegram(RPCHandler): async def _force_exit_inline(self, update: Update, _: CallbackContext) -> None: if update.callback_query: query = update.callback_query - if query.data and '__' in query.data: + if query.data and "__" in query.data: # Input data is "force_exit__" - trade_id = query.data.split("__")[1].split(' ')[0] - if trade_id == 'cancel': + trade_id = query.data.split("__")[1].split(" ")[0] + if trade_id == "cancel": await query.answer() await query.edit_message_text(text="Force exit canceled.") return @@ -1204,17 +1295,20 @@ class Telegram(RPCHandler): await query.answer() if trade: await query.edit_message_text( - text=f"Manually exiting Trade #{trade_id}, {trade.pair}") + text=f"Manually exiting Trade #{trade_id}, {trade.pair}" + ) await self._force_exit_action(trade_id) else: await query.edit_message_text(text=f"Trade {trade_id} not found.") async def _force_enter_action(self, pair, price: Optional[float], order_side: SignalDirection): - if pair != 'cancel': + if pair != "cancel": try: + @safe_async_db def _force_enter(): self._rpc._rpc_force_entry(pair, price, order_side=order_side) + loop = asyncio.get_running_loop() # Workaround to avoid nested loops await loop.run_in_executor(None, _force_enter) @@ -1225,15 +1319,15 @@ class Telegram(RPCHandler): async def _force_enter_inline(self, update: Update, _: CallbackContext) -> None: if update.callback_query: query = update.callback_query - if query.data and '__' in query.data: + if query.data and "__" in query.data: # Input data is "force_enter___" payload = query.data.split("__")[1] - if payload == 'cancel': + if payload == "cancel": await query.answer() await query.edit_message_text(text="Force enter canceled.") return - if payload and '_||_' in payload: - pair, side = payload.split('_||_') + if payload and "_||_" in payload: + pair, side = payload.split("_||_") order_side = SignalDirection(side) await query.answer() await query.edit_message_text(text=f"Manually entering {order_side} for {pair}") @@ -1241,17 +1335,20 @@ class Telegram(RPCHandler): @staticmethod 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)] + 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)] + buttons: List[InlineKeyboardButton], cols=1 + ) -> List[List[InlineKeyboardButton]]: + return [buttons[i : i + cols] for i in range(0, len(buttons), cols)] @authorized_only async def _force_enter( - self, update: Update, context: CallbackContext, order_side: SignalDirection) -> None: + self, update: Update, context: CallbackContext, order_side: SignalDirection + ) -> None: """ Handler for /forcelong and `/forceshort Buys a pair trade at the given or current price @@ -1264,19 +1361,21 @@ class Telegram(RPCHandler): price = float(context.args[1]) if len(context.args) > 1 else None await self._force_enter_action(pair, price, order_side) else: - whitelist = self._rpc._rpc_whitelist()['whitelist'] + whitelist = self._rpc._rpc_whitelist()["whitelist"] pair_buttons = [ InlineKeyboardButton( text=pair, callback_data=f"force_enter__{pair}_||_{order_side}" - ) for pair in sorted(whitelist) + ) + for pair in sorted(whitelist) ] buttons_aligned = self._layout_inline_keyboard(pair_buttons) - buttons_aligned.append([InlineKeyboardButton(text='Cancel', - callback_data='force_enter__cancel')]) - await self._send_msg(msg="Which pair?", - keyboard=buttons_aligned, - query=update.callback_query) + buttons_aligned.append( + [InlineKeyboardButton(text="Cancel", callback_data="force_enter__cancel")] + ) + await self._send_msg( + msg="Which pair?", keyboard=buttons_aligned, query=update.callback_query + ) @authorized_only async def _trades(self, update: Update, context: CallbackContext) -> None: @@ -1287,27 +1386,31 @@ class Telegram(RPCHandler): :param update: message update :return: None """ - stake_cur = self._config['stake_currency'] + stake_cur = self._config["stake_currency"] try: nrecent = int(context.args[0]) if context.args else 10 except (TypeError, ValueError, IndexError): nrecent = 10 - trades = self._rpc._rpc_trade_history( - nrecent - ) + trades = self._rpc._rpc_trade_history(nrecent) trades_tab = tabulate( - [[dt_humanize_delta(dt_from_ts(trade['close_timestamp'])), - trade['pair'] + " (#" + str(trade['trade_id']) + ")", - f"{(trade['close_profit']):.2%} ({trade['close_profit_abs']})"] - for trade in trades['trades']], - headers=[ - 'Close Date', - 'Pair (ID)', - f'Profit ({stake_cur})', + [ + [ + dt_humanize_delta(dt_from_ts(trade["close_timestamp"])), + trade["pair"] + " (#" + str(trade["trade_id"]) + ")", + f"{(trade['close_profit']):.2%} ({trade['close_profit_abs']})", + ] + for trade in trades["trades"] ], - tablefmt='simple') - message = (f"{min(trades['trades_count'], nrecent)} recent trades:\n" - + (f"
{trades_tab}
" if trades['trades_count'] > 0 else '')) + headers=[ + "Close Date", + "Pair (ID)", + f"Profit ({stake_cur})", + ], + tablefmt="simple", + ) + message = f"{min(trades['trades_count'], nrecent)} recent trades:\n" + ( + f"
{trades_tab}
" if trades["trades_count"] > 0 else "" + ) await self._send_msg(message, parse_mode=ParseMode.HTML) @authorized_only @@ -1341,7 +1444,7 @@ class Telegram(RPCHandler): raise RPCException("Trade-id not set.") trade_id = int(context.args[0]) self._rpc._rpc_cancel_open_order(trade_id) - await self._send_msg('Open order canceled.') + await self._send_msg("Open order canceled.") @authorized_only async def _performance(self, update: Update, context: CallbackContext) -> None: @@ -1359,7 +1462,8 @@ class Telegram(RPCHandler): f"{i + 1}.\t {trade['pair']}\t" f"{fmt_coin(trade['profit_abs'], self._config['stake_currency'])} " f"({trade['profit_ratio']:.2%}) " - f"({trade['count']})\n") + f"({trade['count']})\n" + ) if len(output + stat_line) >= MAX_MESSAGE_LENGTH: await self._send_msg(output, parse_mode=ParseMode.HTML) @@ -1367,9 +1471,13 @@ class Telegram(RPCHandler): else: output += stat_line - await self._send_msg(output, parse_mode=ParseMode.HTML, - reload_able=True, callback_path="update_performance", - query=update.callback_query) + await self._send_msg( + output, + parse_mode=ParseMode.HTML, + reload_able=True, + callback_path="update_performance", + query=update.callback_query, + ) @authorized_only async def _enter_tag_performance(self, update: Update, context: CallbackContext) -> None: @@ -1391,7 +1499,8 @@ class Telegram(RPCHandler): f"{i + 1}.\t `{trade['enter_tag']}\t" f"{fmt_coin(trade['profit_abs'], self._config['stake_currency'])} " f"({trade['profit_ratio']:.2%}) " - f"({trade['count']})`\n") + f"({trade['count']})`\n" + ) if len(output + stat_line) >= MAX_MESSAGE_LENGTH: await self._send_msg(output, parse_mode=ParseMode.MARKDOWN) @@ -1399,9 +1508,13 @@ class Telegram(RPCHandler): else: output += stat_line - await self._send_msg(output, parse_mode=ParseMode.MARKDOWN, - reload_able=True, callback_path="update_enter_tag_performance", - query=update.callback_query) + await self._send_msg( + output, + parse_mode=ParseMode.MARKDOWN, + reload_able=True, + callback_path="update_enter_tag_performance", + query=update.callback_query, + ) @authorized_only async def _exit_reason_performance(self, update: Update, context: CallbackContext) -> None: @@ -1423,7 +1536,8 @@ class Telegram(RPCHandler): f"{i + 1}.\t `{trade['exit_reason']}\t" f"{fmt_coin(trade['profit_abs'], self._config['stake_currency'])} " f"({trade['profit_ratio']:.2%}) " - f"({trade['count']})`\n") + f"({trade['count']})`\n" + ) if len(output + stat_line) >= MAX_MESSAGE_LENGTH: await self._send_msg(output, parse_mode=ParseMode.MARKDOWN) @@ -1431,9 +1545,13 @@ class Telegram(RPCHandler): else: output += stat_line - await self._send_msg(output, parse_mode=ParseMode.MARKDOWN, - reload_able=True, callback_path="update_exit_reason_performance", - query=update.callback_query) + await self._send_msg( + output, + parse_mode=ParseMode.MARKDOWN, + reload_able=True, + callback_path="update_exit_reason_performance", + query=update.callback_query, + ) @authorized_only async def _mix_tag_performance(self, update: Update, context: CallbackContext) -> None: @@ -1455,7 +1573,8 @@ class Telegram(RPCHandler): f"{i + 1}.\t `{trade['mix_tag']}\t" f"{fmt_coin(trade['profit_abs'], self._config['stake_currency'])} " f"({trade['profit_ratio']:.2%}) " - f"({trade['count']})`\n") + f"({trade['count']})`\n" + ) if len(output + stat_line) >= MAX_MESSAGE_LENGTH: await self._send_msg(output, parse_mode=ParseMode.MARKDOWN) @@ -1463,9 +1582,13 @@ class Telegram(RPCHandler): else: output += stat_line - await self._send_msg(output, parse_mode=ParseMode.MARKDOWN, - reload_able=True, callback_path="update_mix_tag_performance", - query=update.callback_query) + await self._send_msg( + output, + parse_mode=ParseMode.MARKDOWN, + reload_able=True, + callback_path="update_mix_tag_performance", + query=update.callback_query, + ) @authorized_only async def _count(self, update: Update, context: CallbackContext) -> None: @@ -1477,14 +1600,20 @@ class Telegram(RPCHandler): :return: None """ counts = self._rpc._rpc_count() - message = tabulate({k: [v] for k, v in counts.items()}, - headers=['current', 'max', 'total stake'], - tablefmt='simple') + message = tabulate( + {k: [v] for k, v in counts.items()}, + headers=["current", "max", "total stake"], + tablefmt="simple", + ) message = f"
{message}
" logger.debug(message) - await self._send_msg(message, parse_mode=ParseMode.HTML, - reload_able=True, callback_path="update_count", - query=update.callback_query) + await self._send_msg( + message, + parse_mode=ParseMode.HTML, + reload_able=True, + callback_path="update_count", + query=update.callback_query, + ) @authorized_only async def _locks(self, update: Update, context: CallbackContext) -> None: @@ -1493,17 +1622,18 @@ class Telegram(RPCHandler): Returns the currently active locks """ rpc_locks = self._rpc._rpc_locks() - if not rpc_locks['locks']: - await self._send_msg('No active locks.', parse_mode=ParseMode.HTML) + if not rpc_locks["locks"]: + await self._send_msg("No active locks.", parse_mode=ParseMode.HTML) - for locks in chunks(rpc_locks['locks'], 25): - message = tabulate([[ - lock['id'], - lock['pair'], - lock['lock_end_time'], - lock['reason']] for lock in locks], - headers=['ID', 'Pair', 'Until', 'Reason'], - tablefmt='simple') + for locks in chunks(rpc_locks["locks"], 25): + message = tabulate( + [ + [lock["id"], lock["pair"], lock["lock_end_time"], lock["reason"]] + for lock in locks + ], + headers=["ID", "Pair", "Until", "Reason"], + tablefmt="simple", + ) message = f"
{escape(message)}
" logger.debug(message) await self._send_msg(message, parse_mode=ParseMode.HTML) @@ -1536,9 +1666,9 @@ class Telegram(RPCHandler): if context.args: if "sorted" in context.args: - whitelist['whitelist'] = sorted(whitelist['whitelist']) + whitelist["whitelist"] = sorted(whitelist["whitelist"]) if "baseonly" in context.args: - whitelist['whitelist'] = [pair.split("/")[0] for pair in whitelist['whitelist']] + whitelist["whitelist"] = [pair.split("/")[0] for pair in whitelist["whitelist"]] message = f"Using whitelist `{whitelist['method']}` with {whitelist['length']} pairs\n" message += f"`{', '.join(whitelist['whitelist'])}`" @@ -1556,10 +1686,10 @@ class Telegram(RPCHandler): async def send_blacklist_msg(self, blacklist: Dict): errmsgs = [] - for _, error in blacklist['errors'].items(): + for _, error in blacklist["errors"].items(): errmsgs.append(f"Error: {error['error_msg']}") if errmsgs: - await self._send_msg('\n'.join(errmsgs)) + await self._send_msg("\n".join(errmsgs)) message = f"Blacklist contains {blacklist['length']} pairs\n" message += f"`{', '.join(blacklist['blacklist'])}`" @@ -1585,21 +1715,23 @@ class Telegram(RPCHandler): limit = int(context.args[0]) if context.args else 10 except (TypeError, ValueError, IndexError): limit = 10 - logs = RPC._rpc_get_logs(limit)['logs'] - msgs = '' + logs = RPC._rpc_get_logs(limit)["logs"] + msgs = "" msg_template = "*{}* {}: {} \\- `{}`" for logrec in logs: - msg = msg_template.format(escape_markdown(logrec[0], version=2), - escape_markdown(logrec[2], version=2), - escape_markdown(logrec[3], version=2), - escape_markdown(logrec[4], version=2)) + msg = msg_template.format( + escape_markdown(logrec[0], version=2), + escape_markdown(logrec[2], version=2), + escape_markdown(logrec[3], version=2), + escape_markdown(logrec[4], version=2), + ) if len(msgs + msg) + 10 >= MAX_MESSAGE_LENGTH: # Send message immediately if it would become too long await self._send_msg(msgs, parse_mode=ParseMode.MARKDOWN_V2) - msgs = msg + '\n' + msgs = msg + "\n" else: # Append message to messages to send - msgs += msg + '\n' + msgs += msg + "\n" if msgs: await self._send_msg(msgs, parse_mode=ParseMode.MARKDOWN_V2) @@ -1612,13 +1744,14 @@ class Telegram(RPCHandler): """ edge_pairs = self._rpc._rpc_edge() if not edge_pairs: - message = 'Edge only validated following pairs:' + message = "Edge only validated following pairs:" await self._send_msg(message, parse_mode=ParseMode.HTML) for chunk in chunks(edge_pairs, 25): - edge_pairs_tab = tabulate(chunk, headers='keys', tablefmt='simple') - message = (f'Edge only validated following pairs:\n' - f'
{edge_pairs_tab}
') + edge_pairs_tab = tabulate(chunk, headers="keys", tablefmt="simple") + message = ( + f"Edge only validated following pairs:\n" f"
{edge_pairs_tab}
" + ) await self._send_msg(message, parse_mode=ParseMode.HTML) @@ -1631,14 +1764,17 @@ class Telegram(RPCHandler): :param update: message update :return: None """ - force_enter_text = ("*/forcelong []:* `Instantly buys the given pair. " - "Optionally takes a rate at which to buy " - "(only applies to limit orders).` \n" - ) + force_enter_text = ( + "*/forcelong []:* `Instantly buys the given pair. " + "Optionally takes a rate at which to buy " + "(only applies to limit orders).` \n" + ) if self._rpc._freqtrade.trading_mode != TradingMode.SPOT: - force_enter_text += ("*/forceshort []:* `Instantly shorts the given pair. " - "Optionally takes a rate at which to sell " - "(only applies to limit orders).` \n") + force_enter_text += ( + "*/forceshort []:* `Instantly shorts the given pair. " + "Optionally takes a rate at which to sell " + "(only applies to limit orders).` \n" + ) message = ( "_Bot Control_\n" "------------\n" @@ -1654,7 +1790,6 @@ class Telegram(RPCHandler): "*/cancel_open_order :* `Cancels open orders for trade. " "Only valid when the trade has open orders.`\n" "*/coo |all:* `Alias to /cancel_open_order`\n" - "*/whitelist [sorted] [baseonly]:* `Show current whitelist. Optionally in " "order and/or only displaying the base currency of each pairing.`\n" "*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs " @@ -1663,7 +1798,6 @@ class Telegram(RPCHandler): "`Delete pair / pattern from blacklist. Will reset on reload_conf.` \n" "*/reload_config:* `Reload configuration file` \n" "*/unlock :* `Unlock this Pair (or this lock id if it's numeric)`\n" - "_Current state_\n" "------------\n" "*/show_config:* `Show running configuration` \n" @@ -1679,7 +1813,6 @@ class Telegram(RPCHandler): "`the currently set market direction will be output.` \n" "*/list_custom_data :* `List custom_data for Trade ID & Key combo.`\n" "`If no Key is supplied it will list all key-value pairs found for that Trade ID.`" - "_Statistics_\n" "------------\n" "*/status |[table]:* `Lists all open trades`\n" @@ -1702,7 +1835,7 @@ class Telegram(RPCHandler): "Avg. holding durations for buys and sells.`\n" "*/help:* `This help message`\n" "*/version:* `Show version`\n" - ) + ) await self._send_msg(message, parse_mode=ParseMode.MARKDOWN) @@ -1728,9 +1861,9 @@ class Telegram(RPCHandler): :return: None """ strategy_version = self._rpc._freqtrade.strategy.version() - version_string = f'*Version:* `{__version__}`' + version_string = f"*Version:* `{__version__}`" if strategy_version is not None: - version_string += f'\n*Strategy version: * `{strategy_version}`' + version_string += f"\n*Strategy version: * `{strategy_version}`" await self._send_msg(version_string) @@ -1745,7 +1878,7 @@ class Telegram(RPCHandler): """ val = RPC._rpc_show_config(self._config, self._rpc._freqtrade.state) - if val['trailing_stop']: + if val["trailing_stop"]: sl_info = ( f"*Initial Stoploss:* `{val['stoploss']}`\n" f"*Trailing stop positive:* `{val['trailing_stop_positive']}`\n" @@ -1756,7 +1889,7 @@ class Telegram(RPCHandler): else: sl_info = f"*Stoploss:* `{val['stoploss']}`\n" - if val['position_adjustment_enable']: + if val["position_adjustment_enable"]: pa_info = ( f"*Position adjustment:* On\n" f"*Max enter position adjustment:* `{val['max_entry_position_adjustment']}`\n" @@ -1798,9 +1931,7 @@ class Telegram(RPCHandler): results = self._rpc._rpc_list_custom_data(trade_id, key) messages = [] if len(results) > 0: - messages.append( - 'Found custom-data entr' + ('ies: ' if len(results) > 1 else 'y: ') - ) + messages.append("Found custom-data entr" + ("ies: " if len(results) > 1 else "y: ")) for result in results: lines = [ f"*Key:* `{result['cd_key']}`", @@ -1809,7 +1940,7 @@ class Telegram(RPCHandler): f"*Type:* `{result['cd_type']}`", f"*Value:* `{result['cd_value']}`", f"*Create Date:* `{format_date(result['created_at'])}`", - f"*Update Date:* `{format_date(result['updated_at'])}`" + f"*Update Date:* `{format_date(result['updated_at'])}`", ] # Filter empty lines using list-comprehension messages.append("\n".join([line for line in lines if line])) @@ -1827,12 +1958,20 @@ class Telegram(RPCHandler): except RPCException as e: await self._send_msg(str(e)) - async def _update_msg(self, query: CallbackQuery, msg: str, callback_path: str = "", - reload_able: bool = False, parse_mode: str = ParseMode.MARKDOWN) -> None: + async def _update_msg( + self, + query: CallbackQuery, + msg: str, + callback_path: str = "", + reload_able: bool = False, + parse_mode: str = ParseMode.MARKDOWN, + ) -> None: if reload_able: - reply_markup = InlineKeyboardMarkup([ - [InlineKeyboardButton("Refresh", callback_data=callback_path)], - ]) + reply_markup = InlineKeyboardMarkup( + [ + [InlineKeyboardButton("Refresh", callback_data=callback_path)], + ] + ) else: reply_markup = InlineKeyboardMarkup([[]]) msg += f"\nUpdated: {datetime.now().ctime()}" @@ -1841,24 +1980,26 @@ class Telegram(RPCHandler): try: await query.edit_message_text( - text=msg, - parse_mode=parse_mode, - reply_markup=reply_markup + text=msg, parse_mode=parse_mode, reply_markup=reply_markup ) except BadRequest as e: - if 'not modified' in e.message.lower(): + if "not modified" in e.message.lower(): pass else: - logger.warning('TelegramError: %s', e.message) + logger.warning("TelegramError: %s", e.message) except TelegramError as telegram_err: - logger.warning('TelegramError: %s! Giving up on that message.', telegram_err.message) + logger.warning("TelegramError: %s! Giving up on that message.", telegram_err.message) - async def _send_msg(self, msg: str, parse_mode: str = ParseMode.MARKDOWN, - disable_notification: bool = False, - keyboard: Optional[List[List[InlineKeyboardButton]]] = None, - callback_path: str = "", - reload_able: bool = False, - query: Optional[CallbackQuery] = None) -> None: + async def _send_msg( + self, + msg: str, + parse_mode: str = ParseMode.MARKDOWN, + disable_notification: bool = False, + keyboard: Optional[List[List[InlineKeyboardButton]]] = None, + callback_path: str = "", + reload_able: bool = False, + query: Optional[CallbackQuery] = None, + ) -> None: """ Send given markdown message :param msg: message @@ -1868,12 +2009,18 @@ class Telegram(RPCHandler): """ reply_markup: Union[InlineKeyboardMarkup, ReplyKeyboardMarkup] if query: - await self._update_msg(query=query, msg=msg, parse_mode=parse_mode, - callback_path=callback_path, reload_able=reload_able) + await self._update_msg( + query=query, + msg=msg, + parse_mode=parse_mode, + callback_path=callback_path, + reload_able=reload_able, + ) return - if reload_able and self._config['telegram'].get('reload', True): - reply_markup = InlineKeyboardMarkup([ - [InlineKeyboardButton("Refresh", callback_data=callback_path)]]) + if reload_able and self._config["telegram"].get("reload", True): + reply_markup = InlineKeyboardMarkup( + [[InlineKeyboardButton("Refresh", callback_data=callback_path)]] + ) else: if keyboard is not None: reply_markup = InlineKeyboardMarkup(keyboard) @@ -1882,7 +2029,7 @@ class Telegram(RPCHandler): try: try: await self._app.bot.send_message( - self._config['telegram']['chat_id'], + self._config["telegram"]["chat_id"], text=msg, parse_mode=parse_mode, reply_markup=reply_markup, @@ -1892,21 +2039,17 @@ class Telegram(RPCHandler): # Sometimes the telegram server resets the current connection, # if this is the case we send the message again. logger.warning( - 'Telegram NetworkError: %s! Trying one more time.', - network_err.message + "Telegram NetworkError: %s! Trying one more time.", network_err.message ) await self._app.bot.send_message( - self._config['telegram']['chat_id'], + self._config["telegram"]["chat_id"], text=msg, parse_mode=parse_mode, reply_markup=reply_markup, disable_notification=disable_notification, ) except TelegramError as telegram_err: - logger.warning( - 'TelegramError: %s! Giving up on that message.', - telegram_err.message - ) + logger.warning("TelegramError: %s! Giving up on that message.", telegram_err.message) @authorized_only async def _changemarketdir(self, update: Update, context: CallbackContext) -> None: @@ -1932,14 +2075,20 @@ class Telegram(RPCHandler): if new_market_dir is not None: self._rpc._update_market_direction(new_market_dir) - await self._send_msg("Successfully updated market direction" - f" from *{old_market_dir}* to *{new_market_dir}*.") + await self._send_msg( + "Successfully updated market direction" + f" from *{old_market_dir}* to *{new_market_dir}*." + ) else: - raise RPCException("Invalid market direction provided. \n" - "Valid market directions: *long, short, even, none*") + raise RPCException( + "Invalid market direction provided. \n" + "Valid market directions: *long, short, even, none*" + ) elif context.args is not None and len(context.args) == 0: old_market_dir = self._rpc._get_market_direction() await self._send_msg(f"Currently set market direction: *{old_market_dir}*") else: - raise RPCException("Invalid usage of command /marketdir. \n" - "Usage: */marketdir [short | long | even | none]*") + raise RPCException( + "Invalid usage of command /marketdir. \n" + "Usage: */marketdir [short | long | even | none]*" + ) diff --git a/freqtrade/rpc/webhook.py b/freqtrade/rpc/webhook.py index 9b12b7a21..d67d654f0 100644 --- a/freqtrade/rpc/webhook.py +++ b/freqtrade/rpc/webhook.py @@ -1,6 +1,7 @@ """ This module manages webhook communication """ + import logging import time from typing import Any, Dict, Optional @@ -15,11 +16,11 @@ from freqtrade.rpc.rpc_types import RPCSendMsg logger = logging.getLogger(__name__) -logger.debug('Included module rpc.webhook ...') +logger.debug("Included module rpc.webhook ...") class Webhook(RPCHandler): - """ This class handles all webhook communication """ + """This class handles all webhook communication""" def __init__(self, rpc: RPC, config: Config) -> None: """ @@ -30,11 +31,11 @@ class Webhook(RPCHandler): """ super().__init__(rpc, config) - self._url = self._config['webhook']['url'] - self._format = self._config['webhook'].get('format', 'form') - self._retries = self._config['webhook'].get('retries', 0) - self._retry_delay = self._config['webhook'].get('retry_delay', 0.1) - self._timeout = self._config['webhook'].get('timeout', 10) + self._url = self._config["webhook"]["url"] + self._format = self._config["webhook"].get("format", "form") + self._retries = self._config["webhook"].get("retries", 0) + self._retry_delay = self._config["webhook"].get("retry_delay", 0.1) + self._timeout = self._config["webhook"].get("timeout", 10) def cleanup(self) -> None: """ @@ -44,54 +45,59 @@ class Webhook(RPCHandler): pass def _get_value_dict(self, msg: RPCSendMsg) -> Optional[Dict[str, Any]]: - whconfig = self._config['webhook'] - if msg['type'].value in whconfig: + whconfig = self._config["webhook"] + if msg["type"].value in whconfig: # Explicit types should have priority - valuedict = whconfig.get(msg['type'].value) + valuedict = whconfig.get(msg["type"].value) # Deprecated 2022.10 - only keep generic method. - elif msg['type'] in [RPCMessageType.ENTRY]: - valuedict = whconfig.get('webhookentry') - elif msg['type'] in [RPCMessageType.ENTRY_CANCEL]: - valuedict = whconfig.get('webhookentrycancel') - elif msg['type'] in [RPCMessageType.ENTRY_FILL]: - valuedict = whconfig.get('webhookentryfill') - elif msg['type'] == RPCMessageType.EXIT: - valuedict = whconfig.get('webhookexit') - elif msg['type'] == RPCMessageType.EXIT_FILL: - valuedict = whconfig.get('webhookexitfill') - elif msg['type'] == RPCMessageType.EXIT_CANCEL: - valuedict = whconfig.get('webhookexitcancel') - elif msg['type'] in (RPCMessageType.STATUS, - RPCMessageType.STARTUP, - RPCMessageType.EXCEPTION, - RPCMessageType.WARNING): - valuedict = whconfig.get('webhookstatus') - elif msg['type'] in ( - RPCMessageType.PROTECTION_TRIGGER, - RPCMessageType.PROTECTION_TRIGGER_GLOBAL, - RPCMessageType.WHITELIST, - RPCMessageType.ANALYZED_DF, - RPCMessageType.NEW_CANDLE, - RPCMessageType.STRATEGY_MSG): + elif msg["type"] in [RPCMessageType.ENTRY]: + valuedict = whconfig.get("webhookentry") + elif msg["type"] in [RPCMessageType.ENTRY_CANCEL]: + valuedict = whconfig.get("webhookentrycancel") + elif msg["type"] in [RPCMessageType.ENTRY_FILL]: + valuedict = whconfig.get("webhookentryfill") + elif msg["type"] == RPCMessageType.EXIT: + valuedict = whconfig.get("webhookexit") + elif msg["type"] == RPCMessageType.EXIT_FILL: + valuedict = whconfig.get("webhookexitfill") + elif msg["type"] == RPCMessageType.EXIT_CANCEL: + valuedict = whconfig.get("webhookexitcancel") + elif msg["type"] in ( + RPCMessageType.STATUS, + RPCMessageType.STARTUP, + RPCMessageType.EXCEPTION, + RPCMessageType.WARNING, + ): + valuedict = whconfig.get("webhookstatus") + elif msg["type"] in ( + RPCMessageType.PROTECTION_TRIGGER, + RPCMessageType.PROTECTION_TRIGGER_GLOBAL, + RPCMessageType.WHITELIST, + RPCMessageType.ANALYZED_DF, + RPCMessageType.NEW_CANDLE, + RPCMessageType.STRATEGY_MSG, + ): # Don't fail for non-implemented types return None return valuedict def send_msg(self, msg: RPCSendMsg) -> None: - """ Send a message to telegram channel """ + """Send a message to telegram channel""" try: - valuedict = self._get_value_dict(msg) if not valuedict: - logger.debug("Message type '%s' not configured for webhooks", msg['type']) + logger.debug("Message type '%s' not configured for webhooks", msg["type"]) return payload = {key: value.format(**msg) for (key, value) in valuedict.items()} self._send_msg(payload) except KeyError as exc: - logger.exception("Problem calling Webhook. Please check your webhook configuration. " - "Exception: %s", exc) + logger.exception( + "Problem calling Webhook. Please check your webhook configuration. " + "Exception: %s", + exc, + ) def _send_msg(self, payload: dict) -> None: """do the actual call to the webhook""" @@ -107,16 +113,19 @@ class Webhook(RPCHandler): attempts += 1 try: - if self._format == 'form': + if self._format == "form": response = post(self._url, data=payload, timeout=self._timeout) - elif self._format == 'json': + elif self._format == "json": response = post(self._url, json=payload, timeout=self._timeout) - elif self._format == 'raw': - response = post(self._url, data=payload['data'], - headers={'Content-Type': 'text/plain'}, - timeout=self._timeout) + elif self._format == "raw": + response = post( + self._url, + data=payload["data"], + headers={"Content-Type": "text/plain"}, + timeout=self._timeout, + ) else: - raise NotImplementedError(f'Unknown format: {self._format}') + raise NotImplementedError(f"Unknown format: {self._format}") # Throw a RequestException if the post was not successful response.raise_for_status()