From 73e182260ed5029b952df787ebe0f980f4d0a9e3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 12 May 2024 16:32:47 +0200 Subject: [PATCH] ruff format: more files --- freqtrade/misc.py | 36 +-- freqtrade/plot/plotting.py | 499 +++++++++++++++++++------------------ freqtrade/wallets.py | 105 ++++---- freqtrade/worker.py | 71 +++--- 4 files changed, 368 insertions(+), 343 deletions(-) diff --git a/freqtrade/misc.py b/freqtrade/misc.py index a6c6f15fd..9a33fe430 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -1,6 +1,7 @@ """ Various tool function for Freqtrade and scripts """ + import gzip import logging from io import StringIO @@ -27,17 +28,17 @@ def file_dump_json(filename: Path, data: Any, is_zip: bool = False, log: bool = """ if is_zip: - if filename.suffix != '.gz': - filename = filename.with_suffix('.gz') + if filename.suffix != ".gz": + filename = filename.with_suffix(".gz") if log: logger.info(f'dumping json to "{filename}"') - with gzip.open(filename, 'w') as fpz: + with gzip.open(filename, "w") as fpz: rapidjson.dump(data, fpz, default=str, number_mode=rapidjson.NM_NATIVE) else: if log: logger.info(f'dumping json to "{filename}"') - with filename.open('w') as fp: + with filename.open("w") as fp: rapidjson.dump(data, fp, default=str, number_mode=rapidjson.NM_NATIVE) logger.debug(f'done json to "{filename}"') @@ -54,7 +55,7 @@ def file_dump_joblib(filename: Path, data: Any, log: bool = True) -> None: if log: logger.info(f'dumping joblib to "{filename}"') - with filename.open('wb') as fp: + with filename.open("wb") as fp: joblib.dump(data, fp) logger.debug(f'done joblib dump to "{filename}"') @@ -69,9 +70,8 @@ def json_load(datafile: Union[gzip.GzipFile, TextIO]) -> Any: def file_load_json(file: Path): - if file.suffix != ".gz": - gzipfile = file.with_suffix(file.suffix + '.gz') + gzipfile = file.with_suffix(file.suffix + ".gz") else: gzipfile = file # Try gzip file first, otherwise regular json file. @@ -96,8 +96,8 @@ def is_file_in_dir(file: Path, directory: Path) -> bool: def pair_to_filename(pair: str) -> str: - for ch in ['/', ' ', '.', '@', '$', '+', ':']: - pair = pair.replace(ch, '_') + for ch in ["/", " ", ".", "@", "$", "+", ":"]: + pair = pair.replace(ch, "_") return pair @@ -161,7 +161,7 @@ def safe_value_fallback2(dict1: dictMap, dict2: dictMap, key1: str, key2: str, d def plural(num: float, singular: str, plural: Optional[str] = None) -> str: - return singular if (num == 1 or num == -1) else plural or singular + 's' + return singular if (num == 1 or num == -1) else plural or singular + "s" def chunks(lst: List[Any], n: int) -> Iterator[List[Any]]: @@ -172,7 +172,7 @@ def chunks(lst: List[Any], n: int) -> Iterator[List[Any]]: :return: None """ for chunk in range(0, len(lst), n): - yield (lst[chunk:chunk + n]) + yield (lst[chunk : chunk + n]) def parse_db_uri_for_logging(uri: str): @@ -184,8 +184,8 @@ def parse_db_uri_for_logging(uri: str): parsed_db_uri = urlparse(uri) if not parsed_db_uri.netloc: # No need for censoring as no password was provided return uri - pwd = parsed_db_uri.netloc.split(':')[1].split('@')[0] - return parsed_db_uri.geturl().replace(f':{pwd}@', ':*****@') + pwd = parsed_db_uri.netloc.split(":")[1].split("@")[0] + return parsed_db_uri.geturl().replace(f":{pwd}@", ":*****@") def dataframe_to_json(dataframe: pd.DataFrame) -> str: @@ -194,7 +194,7 @@ def dataframe_to_json(dataframe: pd.DataFrame) -> str: :param dataframe: A pandas DataFrame :returns: A JSON string of the pandas DataFrame """ - return dataframe.to_json(orient='split') + return dataframe.to_json(orient="split") def json_to_dataframe(data: str) -> pd.DataFrame: @@ -203,9 +203,9 @@ def json_to_dataframe(data: str) -> pd.DataFrame: :param data: A JSON string :returns: A pandas DataFrame from the JSON string """ - dataframe = pd.read_json(StringIO(data), orient='split') - if 'date' in dataframe.columns: - dataframe['date'] = pd.to_datetime(dataframe['date'], unit='ms', utc=True) + dataframe = pd.read_json(StringIO(data), orient="split") + if "date" in dataframe.columns: + dataframe["date"] = pd.to_datetime(dataframe["date"], unit="ms", utc=True) return dataframe @@ -234,7 +234,7 @@ def append_candles_to_dataframe(left: pd.DataFrame, right: pd.DataFrame) -> pd.D :param right: The new dataframe containing the data you want appended :returns: The dataframe with the right data in it """ - if left.iloc[-1]['date'] != right.iloc[-1]['date']: + if left.iloc[-1]["date"] != right.iloc[-1]["date"]: left = pd.concat([left, right]) # Only keep the last 1500 candles in memory diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index aa3eea3ec..acb3a1999 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -50,55 +50,57 @@ def init_plotscript(config, markets: List, startup_candles: int = 0): """ if "pairs" in config: - pairs = expand_pairlist(config['pairs'], markets) + pairs = expand_pairlist(config["pairs"], markets) else: - pairs = expand_pairlist(config['exchange']['pair_whitelist'], markets) + pairs = expand_pairlist(config["exchange"]["pair_whitelist"], markets) # Set timerange to use - timerange = TimeRange.parse_timerange(config.get('timerange')) + timerange = TimeRange.parse_timerange(config.get("timerange")) data = load_data( - datadir=config.get('datadir'), + datadir=config.get("datadir"), pairs=pairs, - timeframe=config['timeframe'], + timeframe=config["timeframe"], timerange=timerange, startup_candles=startup_candles, - 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), ) if startup_candles and data: min_date, max_date = get_timerange(data) logger.info(f"Loading data from {min_date} to {max_date}") - timerange.adjust_start_if_necessary(timeframe_to_seconds(config['timeframe']), - startup_candles, min_date) + timerange.adjust_start_if_necessary( + timeframe_to_seconds(config["timeframe"]), startup_candles, min_date + ) no_trades = False filename = config.get("exportfilename") if config.get("no_trades", False): no_trades = True - elif config['trade_source'] == 'file': + elif config["trade_source"] == "file": if not filename.is_dir() and not filename.is_file(): logger.warning("Backtest file is missing skipping trades.") no_trades = True try: trades = load_trades( - config['trade_source'], - db_url=config.get('db_url'), + config["trade_source"], + db_url=config.get("db_url"), exportfilename=filename, no_trades=no_trades, - strategy=config.get('strategy'), + strategy=config.get("strategy"), ) except ValueError as e: raise OperationalException(e) from e if not trades.empty: - trades = trim_dataframe(trades, timerange, df_date_col='open_date') + trades = trim_dataframe(trades, timerange, df_date_col="open_date") - return {"ohlcv": data, - "trades": trades, - "pairs": pairs, - "timerange": timerange, - } + return { + "ohlcv": data, + "trades": trades, + "pairs": pairs, + "timerange": timerange, + } def add_indicators(fig, row, indicators: Dict[str, Dict], data: pd.DataFrame) -> make_subplots: @@ -111,38 +113,40 @@ def add_indicators(fig, row, indicators: Dict[str, Dict], data: pd.DataFrame) -> :param data: candlestick DataFrame """ plot_kinds = { - 'scatter': go.Scatter, - 'bar': go.Bar, + "scatter": go.Scatter, + "bar": go.Bar, } for indicator, conf in indicators.items(): logger.debug(f"indicator {indicator} with config {conf}") if indicator in data: - kwargs = {'x': data['date'], - 'y': data[indicator].values, - 'name': indicator - } + kwargs = {"x": data["date"], "y": data[indicator].values, "name": indicator} - plot_type = conf.get('type', 'scatter') - color = conf.get('color') - if plot_type == 'bar': - kwargs.update({'marker_color': color or 'DarkSlateGrey', - 'marker_line_color': color or 'DarkSlateGrey'}) + plot_type = conf.get("type", "scatter") + color = conf.get("color") + if plot_type == "bar": + kwargs.update( + { + "marker_color": color or "DarkSlateGrey", + "marker_line_color": color or "DarkSlateGrey", + } + ) else: if color: - kwargs.update({'line': {'color': color}}) - kwargs['mode'] = 'lines' - if plot_type != 'scatter': - logger.warning(f'Indicator {indicator} has unknown plot trace kind {plot_type}' - f', assuming "scatter".') + kwargs.update({"line": {"color": color}}) + kwargs["mode"] = "lines" + if plot_type != "scatter": + logger.warning( + f"Indicator {indicator} has unknown plot trace kind {plot_type}" + f', assuming "scatter".' + ) - kwargs.update(conf.get('plotly', {})) + kwargs.update(conf.get("plotly", {})) trace = plot_kinds[plot_type](**kwargs) fig.add_trace(trace, row, 1) else: logger.info( - 'Indicator "%s" ignored. Reason: This indicator is not found ' - 'in your strategy.', - indicator + 'Indicator "%s" ignored. Reason: This indicator is not found ' "in your strategy.", + indicator, ) return fig @@ -168,33 +172,27 @@ def add_profit(fig, row, data: pd.DataFrame, column: str, name: str) -> make_sub return fig -def add_max_drawdown(fig, row, trades: pd.DataFrame, df_comb: pd.DataFrame, - timeframe: str, starting_balance: float) -> make_subplots: +def add_max_drawdown( + fig, row, trades: pd.DataFrame, df_comb: pd.DataFrame, timeframe: str, starting_balance: float +) -> make_subplots: """ Add scatter points indicating max drawdown """ try: _, highdate, lowdate, _, _, max_drawdown = calculate_max_drawdown( - trades, - starting_balance=starting_balance + trades, starting_balance=starting_balance ) drawdown = go.Scatter( x=[highdate, lowdate], y=[ - df_comb.loc[timeframe_to_prev_date(timeframe, highdate), 'cum_profit'], - df_comb.loc[timeframe_to_prev_date(timeframe, lowdate), 'cum_profit'], + df_comb.loc[timeframe_to_prev_date(timeframe, highdate), "cum_profit"], + df_comb.loc[timeframe_to_prev_date(timeframe, lowdate), "cum_profit"], ], - mode='markers', + mode="markers", name=f"Max drawdown {max_drawdown:.2%}", text=f"Max drawdown {max_drawdown:.2%}", - marker=dict( - symbol='square-open', - size=9, - line=dict(width=2), - color='green' - - ) + marker=dict(symbol="square-open", size=9, line=dict(width=2), color="green"), ) fig.add_trace(drawdown, row, 1) except ValueError: @@ -208,27 +206,25 @@ def add_underwater(fig, row, trades: pd.DataFrame, starting_balance: float) -> m """ try: underwater = calculate_underwater( - trades, - value_col="profit_abs", - starting_balance=starting_balance + trades, value_col="profit_abs", starting_balance=starting_balance ) underwater_plot = go.Scatter( - x=underwater['date'], - y=underwater['drawdown'], + x=underwater["date"], + y=underwater["drawdown"], name="Underwater Plot", - fill='tozeroy', - fillcolor='#cc362b', - line={'color': '#cc362b'} + fill="tozeroy", + fillcolor="#cc362b", + line={"color": "#cc362b"}, ) underwater_plot_relative = go.Scatter( - x=underwater['date'], - y=(-underwater['drawdown_relative']), + x=underwater["date"], + y=(-underwater["drawdown_relative"]), name="Underwater Plot (%)", - fill='tozeroy', - fillcolor='green', - line={'color': 'green'} + fill="tozeroy", + fillcolor="green", + line={"color": "green"}, ) fig.add_trace(underwater_plot, row, 1) @@ -247,11 +243,11 @@ def add_parallelism(fig, row, trades: pd.DataFrame, timeframe: str) -> make_subp drawdown = go.Scatter( x=result.index, - y=result['open_trades'], + y=result["open_trades"], name="Parallel trades", - fill='tozeroy', - fillcolor='#242222', - line={'color': '#242222'}, + fill="tozeroy", + fillcolor="#242222", + line={"color": "#242222"}, ) fig.add_trace(drawdown, row, 1) except ValueError: @@ -266,52 +262,37 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots: # Trades can be empty if trades is not None and len(trades) > 0: # Create description for exit summarizing the trade - trades['desc'] = trades.apply( - lambda row: f"{row['profit_ratio']:.2%}, " + - (f"{row['enter_tag']}, " if row['enter_tag'] is not None else "") + - f"{row['exit_reason']}, " + - f"{row['trade_duration']} min", - axis=1) + trades["desc"] = trades.apply( + lambda row: f"{row['profit_ratio']:.2%}, " + + (f"{row['enter_tag']}, " if row["enter_tag"] is not None else "") + + f"{row['exit_reason']}, " + + f"{row['trade_duration']} min", + axis=1, + ) trade_entries = go.Scatter( x=trades["open_date"], y=trades["open_rate"], - mode='markers', - name='Trade entry', + mode="markers", + name="Trade entry", text=trades["desc"], - marker=dict( - symbol='circle-open', - size=11, - line=dict(width=2), - color='cyan' - - ) + marker=dict(symbol="circle-open", size=11, line=dict(width=2), color="cyan"), ) trade_exits = go.Scatter( - x=trades.loc[trades['profit_ratio'] > 0, "close_date"], - y=trades.loc[trades['profit_ratio'] > 0, "close_rate"], - text=trades.loc[trades['profit_ratio'] > 0, "desc"], - mode='markers', - name='Exit - Profit', - marker=dict( - symbol='square-open', - size=11, - line=dict(width=2), - color='green' - ) + x=trades.loc[trades["profit_ratio"] > 0, "close_date"], + y=trades.loc[trades["profit_ratio"] > 0, "close_rate"], + text=trades.loc[trades["profit_ratio"] > 0, "desc"], + mode="markers", + name="Exit - Profit", + marker=dict(symbol="square-open", size=11, line=dict(width=2), color="green"), ) trade_exits_loss = go.Scatter( - x=trades.loc[trades['profit_ratio'] <= 0, "close_date"], - y=trades.loc[trades['profit_ratio'] <= 0, "close_rate"], - text=trades.loc[trades['profit_ratio'] <= 0, "desc"], - mode='markers', - name='Exit - Loss', - marker=dict( - symbol='square-open', - size=11, - line=dict(width=2), - color='red' - ) + x=trades.loc[trades["profit_ratio"] <= 0, "close_date"], + y=trades.loc[trades["profit_ratio"] <= 0, "close_rate"], + text=trades.loc[trades["profit_ratio"] <= 0, "desc"], + mode="markers", + name="Exit - Loss", + marker=dict(symbol="square-open", size=11, line=dict(width=2), color="red"), ) fig.add_trace(trade_entries, 1, 1) fig.add_trace(trade_exits, 1, 1) @@ -321,8 +302,9 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots: return fig -def create_plotconfig(indicators1: List[str], indicators2: List[str], - plot_config: Dict[str, Dict]) -> Dict[str, Dict]: +def create_plotconfig( + indicators1: List[str], indicators2: List[str], plot_config: Dict[str, Dict] +) -> Dict[str, Dict]: """ Combines indicators 1 and indicators 2 into plot_config if necessary :param indicators1: List containing Main plot indicators @@ -333,34 +315,40 @@ def create_plotconfig(indicators1: List[str], indicators2: List[str], if plot_config: if indicators1: - plot_config['main_plot'] = {ind: {} for ind in indicators1} + plot_config["main_plot"] = {ind: {} for ind in indicators1} if indicators2: - plot_config['subplots'] = {'Other': {ind: {} for ind in indicators2}} + plot_config["subplots"] = {"Other": {ind: {} for ind in indicators2}} if not plot_config: # If no indicators and no plot-config given, use defaults. if not indicators1: - indicators1 = ['sma', 'ema3', 'ema5'] + indicators1 = ["sma", "ema3", "ema5"] if not indicators2: - indicators2 = ['macd', 'macdsignal'] + indicators2 = ["macd", "macdsignal"] # Create subplot configuration if plot_config is not available. plot_config = { - 'main_plot': {ind: {} for ind in indicators1}, - 'subplots': {'Other': {ind: {} for ind in indicators2}}, + "main_plot": {ind: {} for ind in indicators1}, + "subplots": {"Other": {ind: {} for ind in indicators2}}, } - if 'main_plot' not in plot_config: - plot_config['main_plot'] = {} + if "main_plot" not in plot_config: + plot_config["main_plot"] = {} - if 'subplots' not in plot_config: - plot_config['subplots'] = {} + if "subplots" not in plot_config: + plot_config["subplots"] = {} return plot_config -def plot_area(fig, row: int, data: pd.DataFrame, indicator_a: str, - indicator_b: str, label: str = "", - fill_color: str = "rgba(0,176,246,0.2)") -> make_subplots: - """ Creates a plot for the area between two traces and adds it to fig. +def plot_area( + fig, + row: int, + data: pd.DataFrame, + indicator_a: str, + indicator_b: str, + label: str = "", + fill_color: str = "rgba(0,176,246,0.2)", +) -> make_subplots: + """Creates a plot for the area between two traces and adds it to fig. :param fig: Plot figure to append to :param row: row number for this plot :param data: candlestick DataFrame @@ -372,21 +360,24 @@ def plot_area(fig, row: int, data: pd.DataFrame, indicator_a: str, """ if indicator_a in data and indicator_b in data: # make lines invisible to get the area plotted, only. - line = {'color': 'rgba(255,255,255,0)'} + line = {"color": "rgba(255,255,255,0)"} # TODO: Figure out why scattergl causes problems plotly/plotly.js#2284 - trace_a = go.Scatter(x=data.date, y=data[indicator_a], - showlegend=False, - line=line) - trace_b = go.Scatter(x=data.date, y=data[indicator_b], name=label, - fill="tonexty", fillcolor=fill_color, - line=line) + trace_a = go.Scatter(x=data.date, y=data[indicator_a], showlegend=False, line=line) + trace_b = go.Scatter( + x=data.date, + y=data[indicator_b], + name=label, + fill="tonexty", + fillcolor=fill_color, + line=line, + ) fig.add_trace(trace_a, row, 1) fig.add_trace(trace_b, row, 1) return fig def add_areas(fig, row: int, data: pd.DataFrame, indicators) -> make_subplots: - """ Adds all area plots (specified in plot_config) to fig. + """Adds all area plots (specified in plot_config) to fig. :param fig: Plot figure to append to :param row: row number for this plot :param data: candlestick DataFrame @@ -395,48 +386,43 @@ def add_areas(fig, row: int, data: pd.DataFrame, indicators) -> make_subplots: :return: fig with added filled_traces plot """ for indicator, ind_conf in indicators.items(): - if 'fill_to' in ind_conf: - indicator_b = ind_conf['fill_to'] + if "fill_to" in ind_conf: + indicator_b = ind_conf["fill_to"] if indicator in data and indicator_b in data: - label = ind_conf.get('fill_label', - f'{indicator}<>{indicator_b}') - fill_color = ind_conf.get('fill_color', 'rgba(0,176,246,0.2)') - fig = plot_area(fig, row, data, indicator, indicator_b, - label=label, fill_color=fill_color) + label = ind_conf.get("fill_label", f"{indicator}<>{indicator_b}") + fill_color = ind_conf.get("fill_color", "rgba(0,176,246,0.2)") + fig = plot_area( + fig, row, data, indicator, indicator_b, label=label, fill_color=fill_color + ) elif indicator not in data: logger.info( 'Indicator "%s" ignored. Reason: This indicator is not ' - 'found in your strategy.', indicator + "found in your strategy.", + indicator, ) elif indicator_b not in data: logger.info( - 'fill_to: "%s" ignored. Reason: This indicator is not ' - 'in your strategy.', indicator_b + 'fill_to: "%s" ignored. Reason: This indicator is not ' "in your strategy.", + indicator_b, ) return fig -def create_scatter( - data, - column_name, - color, - direction -) -> Optional[go.Scatter]: - +def create_scatter(data, column_name, color, direction) -> Optional[go.Scatter]: if column_name in data.columns: df_short = data[data[column_name] == 1] if len(df_short) > 0: shorts = go.Scatter( x=df_short.date, y=df_short.close, - mode='markers', + mode="markers", name=column_name, marker=dict( symbol=f"triangle-{direction}-dot", size=9, line=dict(width=1), color=color, - ) + ), ) return shorts else: @@ -446,10 +432,14 @@ def create_scatter( def generate_candlestick_graph( - pair: str, data: pd.DataFrame, trades: Optional[pd.DataFrame] = None, *, - indicators1: Optional[List[str]] = None, indicators2: Optional[List[str]] = None, - plot_config: Optional[Dict[str, Dict]] = None, - ) -> go.Figure: + pair: str, + data: pd.DataFrame, + trades: Optional[pd.DataFrame] = None, + *, + indicators1: Optional[List[str]] = None, + indicators2: Optional[List[str]] = None, + plot_config: Optional[Dict[str, Dict]] = None, +) -> go.Figure: """ Generate the graph from the data generated by Backtesting or from DB Volume will always be plotted in row2, so Row 1 and 3 are to our disposal for custom indicators @@ -466,8 +456,8 @@ def generate_candlestick_graph( indicators2 or [], plot_config or {}, ) - rows = 2 + len(plot_config['subplots']) - row_widths = [1 for _ in plot_config['subplots']] + rows = 2 + len(plot_config["subplots"]) + row_widths = [1 for _ in plot_config["subplots"]] # Define the graph fig = make_subplots( rows=rows, @@ -476,127 +466,131 @@ def generate_candlestick_graph( row_width=row_widths + [1, 4], vertical_spacing=0.0001, ) - fig['layout'].update(title=pair) - fig['layout']['yaxis1'].update(title='Price') - fig['layout']['yaxis2'].update(title='Volume') - for i, name in enumerate(plot_config['subplots']): - fig['layout'][f'yaxis{3 + i}'].update(title=name) - fig['layout']['xaxis']['rangeslider'].update(visible=False) + fig["layout"].update(title=pair) + fig["layout"]["yaxis1"].update(title="Price") + fig["layout"]["yaxis2"].update(title="Volume") + for i, name in enumerate(plot_config["subplots"]): + fig["layout"][f"yaxis{3 + i}"].update(title=name) + fig["layout"]["xaxis"]["rangeslider"].update(visible=False) fig.update_layout(modebar_add=["v1hovermode", "toggleSpikeLines"]) # Common information candles = go.Candlestick( - x=data.date, - open=data.open, - high=data.high, - low=data.low, - close=data.close, - name='Price' + x=data.date, open=data.open, high=data.high, low=data.low, close=data.close, name="Price" ) fig.add_trace(candles, 1, 1) - longs = create_scatter(data, 'enter_long', 'green', 'up') - exit_longs = create_scatter(data, 'exit_long', 'red', 'down') - shorts = create_scatter(data, 'enter_short', 'blue', 'down') - exit_shorts = create_scatter(data, 'exit_short', 'violet', 'up') + longs = create_scatter(data, "enter_long", "green", "up") + exit_longs = create_scatter(data, "exit_long", "red", "down") + shorts = create_scatter(data, "enter_short", "blue", "down") + exit_shorts = create_scatter(data, "exit_short", "violet", "up") for scatter in [longs, exit_longs, shorts, exit_shorts]: if scatter: fig.add_trace(scatter, 1, 1) # Add Bollinger Bands - fig = plot_area(fig, 1, data, 'bb_lowerband', 'bb_upperband', - label="Bollinger Band") + fig = plot_area(fig, 1, data, "bb_lowerband", "bb_upperband", label="Bollinger Band") # prevent bb_lower and bb_upper from plotting try: - del plot_config['main_plot']['bb_lowerband'] - del plot_config['main_plot']['bb_upperband'] + del plot_config["main_plot"]["bb_lowerband"] + del plot_config["main_plot"]["bb_upperband"] except KeyError: pass # main plot goes to row 1 - fig = add_indicators(fig=fig, row=1, indicators=plot_config['main_plot'], data=data) - fig = add_areas(fig, 1, data, plot_config['main_plot']) + fig = add_indicators(fig=fig, row=1, indicators=plot_config["main_plot"], data=data) + fig = add_areas(fig, 1, data, plot_config["main_plot"]) fig = plot_trades(fig, trades) # sub plot: Volume goes to row 2 volume = go.Bar( - x=data['date'], - y=data['volume'], - name='Volume', - marker_color='DarkSlateGrey', - marker_line_color='DarkSlateGrey' + x=data["date"], + y=data["volume"], + name="Volume", + marker_color="DarkSlateGrey", + marker_line_color="DarkSlateGrey", ) fig.add_trace(volume, 2, 1) # add each sub plot to a separate row - for i, label in enumerate(plot_config['subplots']): - sub_config = plot_config['subplots'][label] + for i, label in enumerate(plot_config["subplots"]): + sub_config = plot_config["subplots"][label] row = 3 + i - fig = add_indicators(fig=fig, row=row, indicators=sub_config, - data=data) + fig = add_indicators(fig=fig, row=row, indicators=sub_config, data=data) # fill area between indicators ( 'fill_to': 'other_indicator') fig = add_areas(fig, row, data, sub_config) return fig -def generate_profit_graph(pairs: str, data: Dict[str, pd.DataFrame], - trades: pd.DataFrame, timeframe: str, stake_currency: str, - starting_balance: float) -> go.Figure: +def generate_profit_graph( + pairs: str, + data: Dict[str, pd.DataFrame], + trades: pd.DataFrame, + timeframe: str, + stake_currency: str, + starting_balance: float, +) -> go.Figure: # Combine close-values for all pairs, rename columns to "pair" try: df_comb = combine_dataframes_with_mean(data, "close") except ValueError: raise OperationalException( "No data found. Please make sure that data is available for " - "the timerange and pairs selected.") + "the timerange and pairs selected." + ) # Trim trades to available OHLCV data trades = extract_trades_of_period(df_comb, trades, date_index=True) if len(trades) == 0: - raise OperationalException('No trades found in selected timerange.') + raise OperationalException("No trades found in selected timerange.") # Add combined cumulative profit - df_comb = create_cum_profit(df_comb, trades, 'cum_profit', timeframe) + df_comb = create_cum_profit(df_comb, trades, "cum_profit", timeframe) # Plot the pairs average close prices, and total profit growth avgclose = go.Scatter( x=df_comb.index, - y=df_comb['mean'], - name='Avg close price', + y=df_comb["mean"], + name="Avg close price", ) - fig = make_subplots(rows=6, cols=1, shared_xaxes=True, - row_heights=[1, 1, 1, 0.5, 0.75, 0.75], - vertical_spacing=0.05, - subplot_titles=[ - "AVG Close Price", - "Combined Profit", - "Profit per pair", - "Parallelism", - "Underwater", - "Relative Drawdown", - ]) - fig['layout'].update(title="Freqtrade Profit plot") - fig['layout']['yaxis1'].update(title='Price') - fig['layout']['yaxis2'].update(title=f'Profit {stake_currency}') - fig['layout']['yaxis3'].update(title=f'Profit {stake_currency}') - fig['layout']['yaxis4'].update(title='Trade count') - fig['layout']['yaxis5'].update(title='Underwater Plot') - fig['layout']['yaxis6'].update(title='Underwater Plot Relative (%)', tickformat=',.2%') - fig['layout']['xaxis']['rangeslider'].update(visible=False) + fig = make_subplots( + rows=6, + cols=1, + shared_xaxes=True, + row_heights=[1, 1, 1, 0.5, 0.75, 0.75], + vertical_spacing=0.05, + subplot_titles=[ + "AVG Close Price", + "Combined Profit", + "Profit per pair", + "Parallelism", + "Underwater", + "Relative Drawdown", + ], + ) + fig["layout"].update(title="Freqtrade Profit plot") + fig["layout"]["yaxis1"].update(title="Price") + fig["layout"]["yaxis2"].update(title=f"Profit {stake_currency}") + fig["layout"]["yaxis3"].update(title=f"Profit {stake_currency}") + fig["layout"]["yaxis4"].update(title="Trade count") + fig["layout"]["yaxis5"].update(title="Underwater Plot") + fig["layout"]["yaxis6"].update(title="Underwater Plot Relative (%)", tickformat=",.2%") + fig["layout"]["xaxis"]["rangeslider"].update(visible=False) fig.update_layout(modebar_add=["v1hovermode", "toggleSpikeLines"]) fig.add_trace(avgclose, 1, 1) - fig = add_profit(fig, 2, df_comb, 'cum_profit', 'Profit') + fig = add_profit(fig, 2, df_comb, "cum_profit", "Profit") fig = add_max_drawdown(fig, 2, trades, df_comb, timeframe, starting_balance) fig = add_parallelism(fig, 4, trades, timeframe) # Two rows consumed fig = add_underwater(fig, 5, trades, starting_balance) for pair in pairs: - profit_col = f'cum_profit_{pair}' + profit_col = f"cum_profit_{pair}" try: - df_comb = create_cum_profit(df_comb, trades[trades['pair'] == pair], profit_col, - timeframe) + df_comb = create_cum_profit( + df_comb, trades[trades["pair"] == pair], profit_col, timeframe + ) fig = add_profit(fig, 3, df_comb, profit_col, f"Profit {pair}") except ValueError: pass @@ -608,9 +602,9 @@ def generate_plot_filename(pair: str, timeframe: str) -> str: Generate filenames per pair/timeframe to be used for storing plots """ pair_s = pair_to_filename(pair) - file_name = 'freqtrade-plot-' + pair_s + '-' + timeframe + '.html' + file_name = "freqtrade-plot-" + pair_s + "-" + timeframe + ".html" - logger.info('Generate plot file for %s', pair) + logger.info("Generate plot file for %s", pair) return file_name @@ -627,8 +621,7 @@ def store_plot_file(fig, filename: str, directory: Path, auto_open: bool = False directory.mkdir(parents=True, exist_ok=True) _filename = directory.joinpath(filename) - plot(fig, filename=str(_filename), - auto_open=auto_open) + plot(fig, filename=str(_filename), auto_open=auto_open) logger.info(f"Stored plot as {_filename}") @@ -650,17 +643,17 @@ def load_and_plot_trades(config: Config): strategy.ft_bot_start() strategy_safe_wrapper(strategy.bot_loop_start)(current_time=datetime.now(timezone.utc)) plot_elements = init_plotscript(config, list(exchange.markets), strategy.startup_candle_count) - timerange = plot_elements['timerange'] - trades = plot_elements['trades'] + timerange = plot_elements["timerange"] + trades = plot_elements["trades"] pair_counter = 0 for pair, data in plot_elements["ohlcv"].items(): pair_counter += 1 logger.info("analyse pair %s", pair) - df_analyzed = strategy.analyze_ticker(data, {'pair': pair}) + df_analyzed = strategy.analyze_ticker(data, {"pair": pair}) df_analyzed = trim_dataframe(df_analyzed, timerange) if not trades.empty: - trades_pair = trades.loc[trades['pair'] == pair] + trades_pair = trades.loc[trades["pair"] == pair] trades_pair = extract_trades_of_period(df_analyzed, trades_pair) else: trades_pair = trades @@ -669,15 +662,18 @@ def load_and_plot_trades(config: Config): pair=pair, data=df_analyzed, trades=trades_pair, - indicators1=config.get('indicators1', []), - indicators2=config.get('indicators2', []), - plot_config=strategy.plot_config if hasattr(strategy, 'plot_config') else {} + indicators1=config.get("indicators1", []), + indicators2=config.get("indicators2", []), + plot_config=strategy.plot_config if hasattr(strategy, "plot_config") else {}, ) - store_plot_file(fig, filename=generate_plot_filename(pair, config['timeframe']), - directory=config['user_data_dir'] / 'plot') + store_plot_file( + fig, + filename=generate_plot_filename(pair, config["timeframe"]), + directory=config["user_data_dir"] / "plot", + ) - logger.info('End of plotting process. %s plots generated', pair_counter) + logger.info("End of plotting process. %s plots generated", pair_counter) def plot_profit(config: Config) -> None: @@ -687,28 +683,37 @@ def plot_profit(config: Config) -> None: But should be somewhat proportional, and therefore useful in helping out to find a good algorithm. """ - if 'timeframe' not in config: - raise OperationalException('Timeframe must be set in either config or via --timeframe.') + if "timeframe" not in config: + raise OperationalException("Timeframe must be set in either config or via --timeframe.") exchange = ExchangeResolver.load_exchange(config) plot_elements = init_plotscript(config, list(exchange.markets)) - trades = plot_elements['trades'] + trades = plot_elements["trades"] # Filter trades to relevant pairs # Remove open pairs - we don't know the profit yet so can't calculate profit for these. # Also, If only one open pair is left, then the profit-generation would fail. - trades = trades[(trades['pair'].isin(plot_elements['pairs'])) - & (~trades['close_date'].isnull()) - ] + trades = trades[ + (trades["pair"].isin(plot_elements["pairs"])) & (~trades["close_date"].isnull()) + ] if len(trades) == 0: - raise OperationalException("No trades found, cannot generate Profit-plot without " - "trades from either Backtest result or database.") + raise OperationalException( + "No trades found, cannot generate Profit-plot without " + "trades from either Backtest result or database." + ) # Create an average close price of all the pairs that were involved. # this could be useful to gauge the overall market trend - fig = generate_profit_graph(plot_elements['pairs'], plot_elements['ohlcv'], - trades, config['timeframe'], - config.get('stake_currency', ''), - config.get('available_capital', config['dry_run_wallet'])) - store_plot_file(fig, filename='freqtrade-profit-plot.html', - directory=config['user_data_dir'] / 'plot', - auto_open=config.get('plot_auto_open', False)) + fig = generate_profit_graph( + plot_elements["pairs"], + plot_elements["ohlcv"], + trades, + config["timeframe"], + config.get("stake_currency", ""), + config.get("available_capital", config["dry_run_wallet"]), + ) + store_plot_file( + fig, + filename="freqtrade-profit-plot.html", + directory=config["user_data_dir"] / "plot", + auto_open=config.get("plot_auto_open", False), + ) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index a1038b368..7f839cb24 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -1,5 +1,5 @@ # pragma pylint: disable=W0603 -""" Wallet """ +"""Wallet""" import logging from copy import deepcopy @@ -31,18 +31,17 @@ class PositionWallet(NamedTuple): position: float = 0 leverage: float = 0 collateral: float = 0 - side: str = 'long' + side: str = "long" class Wallets: - def __init__(self, config: Config, exchange: Exchange, is_backtest: bool = False) -> None: self._config = config self._is_backtest = is_backtest self._exchange = exchange self._wallets: Dict[str, Wallet] = {} self._positions: Dict[str, PositionWallet] = {} - self.start_cap = config['dry_run_wallet'] + self.start_cap = config["dry_run_wallet"] self._last_wallet_refresh: Optional[datetime] = None self.update() @@ -88,17 +87,12 @@ class Wallets: tot_in_trades = sum(trade.stake_amount for trade in open_trades) used_stake = 0.0 - if self._config.get('trading_mode', 'spot') != TradingMode.FUTURES: + if self._config.get("trading_mode", "spot") != TradingMode.FUTURES: current_stake = self.start_cap + tot_profit - tot_in_trades total_stake = current_stake for trade in open_trades: curr = self._exchange.get_pair_base_currency(trade.pair) - _wallets[curr] = Wallet( - curr, - trade.amount, - 0, - trade.amount - ) + _wallets[curr] = Wallet(curr, trade.amount, 0, trade.amount) else: tot_in_trades = 0 for position in open_trades: @@ -108,20 +102,21 @@ class Wallets: leverage = position.leverage tot_in_trades += collateral _positions[position.pair] = PositionWallet( - position.pair, position=size, + position.pair, + position=size, leverage=leverage, collateral=collateral, - side=position.trade_direction + side=position.trade_direction, ) current_stake = self.start_cap + tot_profit - tot_in_trades used_stake = tot_in_trades total_stake = current_stake + tot_in_trades - _wallets[self._config['stake_currency']] = Wallet( - currency=self._config['stake_currency'], + _wallets[self._config["stake_currency"]] = Wallet( + currency=self._config["stake_currency"], free=current_stake, used=used_stake, - total=total_stake + total=total_stake, ) self._wallets = _wallets self._positions = _positions @@ -133,9 +128,9 @@ class Wallets: if isinstance(balances[currency], dict): self._wallets[currency] = Wallet( currency, - balances[currency].get('free'), - balances[currency].get('used'), - balances[currency].get('total') + balances[currency].get("free"), + balances[currency].get("used"), + balances[currency].get("total"), ) # Remove currencies no longer in get_balances output for currency in deepcopy(self._wallets): @@ -145,18 +140,19 @@ class Wallets: positions = self._exchange.fetch_positions() self._positions = {} for position in positions: - symbol = position['symbol'] - if position['side'] is None or position['collateral'] == 0.0: + symbol = position["symbol"] + if position["side"] is None or position["collateral"] == 0.0: # Position is not open ... continue - size = self._exchange._contracts_to_amount(symbol, position['contracts']) - collateral = safe_value_fallback(position, 'collateral', 'initialMargin', 0.0) - leverage = position['leverage'] + size = self._exchange._contracts_to_amount(symbol, position["contracts"]) + collateral = safe_value_fallback(position, "collateral", "initialMargin", 0.0) + leverage = position["leverage"] self._positions[symbol] = PositionWallet( - symbol, position=size, + symbol, + position=size, leverage=leverage, collateral=collateral, - side=position['side'] + side=position["side"], ) def update(self, require_update: bool = True) -> None: @@ -173,12 +169,12 @@ class Wallets: or self._last_wallet_refresh is None or (self._last_wallet_refresh + timedelta(seconds=3600) < now) ): - if (not self._config['dry_run'] or self._config.get('runmode') == RunMode.LIVE): + if not self._config["dry_run"] or self._config.get("runmode") == RunMode.LIVE: self._update_live() else: self._update_dry() if not self._is_backtest: - logger.info('Wallets synced.') + logger.info("Wallets synced.") self._last_wallet_refresh = dt_now() def get_all_balances(self) -> Dict[str, Wallet]: @@ -222,11 +218,11 @@ class Wallets: or by using current balance subtracting """ if "available_capital" in self._config: - return self._config['available_capital'] + return self._config["available_capital"] else: tot_profit = Trade.get_total_closed_profit() open_stakes = Trade.total_open_trades_stakes() - available_balance = self.get_free(self._config['stake_currency']) + available_balance = self.get_free(self._config["stake_currency"]) return available_balance - tot_profit + open_stakes def get_total_stake_amount(self): @@ -238,7 +234,7 @@ class Wallets: """ val_tied_up = Trade.total_open_trades_stakes() if "available_capital" in self._config: - starting_balance = self._config['available_capital'] + starting_balance = self._config["available_capital"] tot_profit = Trade.get_total_closed_profit() available_amount = starting_balance + tot_profit @@ -246,8 +242,9 @@ class Wallets: # Ensure % is used from the overall balance # Otherwise we'd risk lowering stakes with each open trade. # (tied up + current free) * ratio) - tied up - available_amount = ((val_tied_up + self.get_free(self._config['stake_currency'])) * - self._config['tradable_balance_ratio']) + available_amount = ( + val_tied_up + self.get_free(self._config["stake_currency"]) + ) * self._config["tradable_balance_ratio"] return available_amount def get_available_stake_amount(self) -> float: @@ -258,11 +255,12 @@ class Wallets: ( + free amount) * tradable_balance_ratio - """ - free = self.get_free(self._config['stake_currency']) + free = self.get_free(self._config["stake_currency"]) return min(self.get_total_stake_amount() - Trade.total_open_trades_stakes(), free) - def _calculate_unlimited_stake_amount(self, available_amount: float, - val_tied_up: float, max_open_trades: IntOrInf) -> float: + def _calculate_unlimited_stake_amount( + self, available_amount: float, val_tied_up: float, max_open_trades: IntOrInf + ) -> float: """ Calculate stake amount for "unlimited" stake amount :return: 0 if max number of trades reached, else stake_amount to use. @@ -282,10 +280,10 @@ class Wallets: :raise: DependencyException if balance is lower than stake-amount """ - if self._config['amend_last_stake_amount']: + if self._config["amend_last_stake_amount"]: # Remaining amount needs to be at least stake_amount * last_stake_amount_min_ratio # Otherwise the remaining amount is too low to trade. - if available_amount > (stake_amount * self._config['last_stake_amount_min_ratio']): + if available_amount > (stake_amount * self._config["last_stake_amount_min_ratio"]): stake_amount = min(stake_amount, available_amount) else: stake_amount = 0 @@ -299,7 +297,8 @@ class Wallets: return stake_amount def get_trade_stake_amount( - self, pair: str, max_open_trades: IntOrInf, edge=None, update: bool = True) -> float: + self, pair: str, max_open_trades: IntOrInf, edge=None, update: bool = True + ) -> float: """ Calculate stake amount for the trade :return: float: Stake amount @@ -315,21 +314,27 @@ class Wallets: if edge: stake_amount = edge.stake_amount( pair, - self.get_free(self._config['stake_currency']), - self.get_total(self._config['stake_currency']), - val_tied_up + self.get_free(self._config["stake_currency"]), + self.get_total(self._config["stake_currency"]), + val_tied_up, ) else: - stake_amount = self._config['stake_amount'] + stake_amount = self._config["stake_amount"] if stake_amount == UNLIMITED_STAKE_AMOUNT: stake_amount = self._calculate_unlimited_stake_amount( - available_amount, val_tied_up, max_open_trades) + available_amount, val_tied_up, max_open_trades + ) return self._check_available_stake_amount(stake_amount, available_amount) - def validate_stake_amount(self, pair: str, stake_amount: Optional[float], - min_stake_amount: Optional[float], max_stake_amount: float, - trade_amount: Optional[float]): + def validate_stake_amount( + self, + pair: str, + stake_amount: Optional[float], + min_stake_amount: Optional[float], + max_stake_amount: float, + trade_amount: Optional[float], + ): if not stake_amount: logger.debug(f"Stake amount is {stake_amount}, ignoring possible trade for {pair}.") return 0 @@ -342,8 +347,10 @@ class Wallets: if min_stake_amount is not None and min_stake_amount > max_allowed_stake: if not self._is_backtest: - logger.warning("Minimum stake amount > available balance. " - f"{min_stake_amount} > {max_allowed_stake}") + logger.warning( + "Minimum stake amount > available balance. " + f"{min_stake_amount} > {max_allowed_stake}" + ) return 0 if min_stake_amount is not None and stake_amount < min_stake_amount: if not self._is_backtest: diff --git a/freqtrade/worker.py b/freqtrade/worker.py index e9dbfa74b..63ff71277 100644 --- a/freqtrade/worker.py +++ b/freqtrade/worker.py @@ -1,6 +1,7 @@ """ Main Freqtrade worker class. """ + import logging import time import traceback @@ -52,13 +53,15 @@ class Worker: # Init the instance of the bot self.freqtrade = FreqtradeBot(self._config) - internals_config = self._config.get('internals', {}) - self._throttle_secs = internals_config.get('process_throttle_secs', - PROCESS_THROTTLE_SECS) - self._heartbeat_interval = internals_config.get('heartbeat_interval', 60) + internals_config = self._config.get("internals", {}) + self._throttle_secs = internals_config.get("process_throttle_secs", PROCESS_THROTTLE_SECS) + self._heartbeat_interval = internals_config.get("heartbeat_interval", 60) - self._sd_notify = sdnotify.SystemdNotifier() if \ - self._config.get('internals', {}).get('sd_notify', False) else None + self._sd_notify = ( + sdnotify.SystemdNotifier() + if self._config.get("internals", {}).get("sd_notify", False) + else None + ) def _notify(self, message: str) -> None: """ @@ -86,12 +89,12 @@ class Worker: # Log state transition if state != old_state: - if old_state != State.RELOAD_CONFIG: - self.freqtrade.notify_status(f'{state.name.lower()}') + self.freqtrade.notify_status(f"{state.name.lower()}") logger.info( - f"Changing state{f' from {old_state.name}' if old_state else ''} to: {state.name}") + f"Changing state{f' from {old_state.name}' if old_state else ''} to: {state.name}" + ) if state == State.RUNNING: self.freqtrade.startup() @@ -113,26 +116,36 @@ class Worker: self._notify("WATCHDOG=1\nSTATUS=State: RUNNING.") # Use an offset of 1s to ensure a new candle has been issued - self._throttle(func=self._process_running, throttle_secs=self._throttle_secs, - timeframe=self._config['timeframe'] if self._config else None, - timeframe_offset=1) + self._throttle( + func=self._process_running, + throttle_secs=self._throttle_secs, + timeframe=self._config["timeframe"] if self._config else None, + timeframe_offset=1, + ) if self._heartbeat_interval: now = time.time() if (now - self._heartbeat_msg) > self._heartbeat_interval: version = __version__ strategy_version = self.freqtrade.strategy.version() - if (strategy_version is not None): - version += ', strategy_version: ' + strategy_version - logger.info(f"Bot heartbeat. PID={getpid()}, " - f"version='{version}', state='{state.name}'") + if strategy_version is not None: + version += ", strategy_version: " + strategy_version + logger.info( + f"Bot heartbeat. PID={getpid()}, " f"version='{version}', state='{state.name}'" + ) self._heartbeat_msg = now return state - def _throttle(self, func: Callable[..., Any], throttle_secs: float, - timeframe: Optional[str] = None, timeframe_offset: float = 1.0, - *args, **kwargs) -> Any: + def _throttle( + self, + func: Callable[..., Any], + throttle_secs: float, + timeframe: Optional[str] = None, + timeframe_offset: float = 1.0, + *args, + **kwargs, + ) -> Any: """ Throttles the given callable that it takes at least `min_secs` to finish execution. @@ -160,10 +173,11 @@ class Worker: sleep_duration = max(sleep_duration, 0.0) # next_iter = datetime.now(timezone.utc) + timedelta(seconds=sleep_duration) - logger.debug(f"Throttling with '{func.__name__}()': sleep for {sleep_duration:.2f} s, " - f"last iteration took {time_passed:.2f} s." - # f"next: {next_iter}" - ) + logger.debug( + f"Throttling with '{func.__name__}()': sleep for {sleep_duration:.2f} s, " + f"last iteration took {time_passed:.2f} s." + # f"next: {next_iter}" + ) self._sleep(sleep_duration) return result @@ -183,14 +197,13 @@ class Worker: time.sleep(RETRY_TIMEOUT) except OperationalException: tb = traceback.format_exc() - hint = 'Issue `/start` if you think it is safe to restart.' + hint = "Issue `/start` if you think it is safe to restart." self.freqtrade.notify_status( - f'*OperationalException:*\n```\n{tb}```\n {hint}', - msg_type=RPCMessageType.EXCEPTION + f"*OperationalException:*\n```\n{tb}```\n {hint}", msg_type=RPCMessageType.EXCEPTION ) - logger.exception('OperationalException. Stopping trader ...') + logger.exception("OperationalException. Stopping trader ...") self.freqtrade.state = State.STOPPED def _reconfigure(self) -> None: @@ -207,7 +220,7 @@ class Worker: # Load and validate config and create new instance of the bot self._init(True) - self.freqtrade.notify_status('config reloaded') + self.freqtrade.notify_status("config reloaded") # Tell systemd that we completed reconfiguration self._notify("READY=1") @@ -217,5 +230,5 @@ class Worker: self._notify("STOPPING=1") if self.freqtrade: - self.freqtrade.notify_status('process died') + self.freqtrade.notify_status("process died") self.freqtrade.cleanup()