ruff format: more files

This commit is contained in:
Matthias 2024-05-12 16:32:47 +02:00
parent f8f9ac38b2
commit 73e182260e
4 changed files with 368 additions and 343 deletions

View File

@ -1,6 +1,7 @@
""" """
Various tool function for Freqtrade and scripts Various tool function for Freqtrade and scripts
""" """
import gzip import gzip
import logging import logging
from io import StringIO 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 is_zip:
if filename.suffix != '.gz': if filename.suffix != ".gz":
filename = filename.with_suffix('.gz') filename = filename.with_suffix(".gz")
if log: if log:
logger.info(f'dumping json to "{filename}"') 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) rapidjson.dump(data, fpz, default=str, number_mode=rapidjson.NM_NATIVE)
else: else:
if log: if log:
logger.info(f'dumping json to "{filename}"') 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) rapidjson.dump(data, fp, default=str, number_mode=rapidjson.NM_NATIVE)
logger.debug(f'done json to "{filename}"') 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: if log:
logger.info(f'dumping joblib to "{filename}"') logger.info(f'dumping joblib to "{filename}"')
with filename.open('wb') as fp: with filename.open("wb") as fp:
joblib.dump(data, fp) joblib.dump(data, fp)
logger.debug(f'done joblib dump to "{filename}"') 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): def file_load_json(file: Path):
if file.suffix != ".gz": if file.suffix != ".gz":
gzipfile = file.with_suffix(file.suffix + '.gz') gzipfile = file.with_suffix(file.suffix + ".gz")
else: else:
gzipfile = file gzipfile = file
# Try gzip file first, otherwise regular json 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: def pair_to_filename(pair: str) -> str:
for ch in ['/', ' ', '.', '@', '$', '+', ':']: for ch in ["/", " ", ".", "@", "$", "+", ":"]:
pair = pair.replace(ch, '_') pair = pair.replace(ch, "_")
return pair 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: 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]]: 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 :return: None
""" """
for chunk in range(0, len(lst), n): 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): 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) parsed_db_uri = urlparse(uri)
if not parsed_db_uri.netloc: # No need for censoring as no password was provided if not parsed_db_uri.netloc: # No need for censoring as no password was provided
return uri return uri
pwd = parsed_db_uri.netloc.split(':')[1].split('@')[0] pwd = parsed_db_uri.netloc.split(":")[1].split("@")[0]
return parsed_db_uri.geturl().replace(f':{pwd}@', ':*****@') return parsed_db_uri.geturl().replace(f":{pwd}@", ":*****@")
def dataframe_to_json(dataframe: pd.DataFrame) -> str: 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 :param dataframe: A pandas DataFrame
:returns: A JSON string of the 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: 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 :param data: A JSON string
:returns: A pandas DataFrame from the JSON string :returns: A pandas DataFrame from the JSON string
""" """
dataframe = pd.read_json(StringIO(data), orient='split') dataframe = pd.read_json(StringIO(data), orient="split")
if 'date' in dataframe.columns: if "date" in dataframe.columns:
dataframe['date'] = pd.to_datetime(dataframe['date'], unit='ms', utc=True) dataframe["date"] = pd.to_datetime(dataframe["date"], unit="ms", utc=True)
return dataframe 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 :param right: The new dataframe containing the data you want appended
:returns: The dataframe with the right data in it :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]) left = pd.concat([left, right])
# Only keep the last 1500 candles in memory # Only keep the last 1500 candles in memory

View File

@ -50,55 +50,57 @@ def init_plotscript(config, markets: List, startup_candles: int = 0):
""" """
if "pairs" in config: if "pairs" in config:
pairs = expand_pairlist(config['pairs'], markets) pairs = expand_pairlist(config["pairs"], markets)
else: else:
pairs = expand_pairlist(config['exchange']['pair_whitelist'], markets) pairs = expand_pairlist(config["exchange"]["pair_whitelist"], markets)
# Set timerange to use # Set timerange to use
timerange = TimeRange.parse_timerange(config.get('timerange')) timerange = TimeRange.parse_timerange(config.get("timerange"))
data = load_data( data = load_data(
datadir=config.get('datadir'), datadir=config.get("datadir"),
pairs=pairs, pairs=pairs,
timeframe=config['timeframe'], timeframe=config["timeframe"],
timerange=timerange, timerange=timerange,
startup_candles=startup_candles, startup_candles=startup_candles,
data_format=config['dataformat_ohlcv'], data_format=config["dataformat_ohlcv"],
candle_type=config.get('candle_type_def', CandleType.SPOT) candle_type=config.get("candle_type_def", CandleType.SPOT),
) )
if startup_candles and data: if startup_candles and data:
min_date, max_date = get_timerange(data) min_date, max_date = get_timerange(data)
logger.info(f"Loading data from {min_date} to {max_date}") logger.info(f"Loading data from {min_date} to {max_date}")
timerange.adjust_start_if_necessary(timeframe_to_seconds(config['timeframe']), timerange.adjust_start_if_necessary(
startup_candles, min_date) timeframe_to_seconds(config["timeframe"]), startup_candles, min_date
)
no_trades = False no_trades = False
filename = config.get("exportfilename") filename = config.get("exportfilename")
if config.get("no_trades", False): if config.get("no_trades", False):
no_trades = True no_trades = True
elif config['trade_source'] == 'file': elif config["trade_source"] == "file":
if not filename.is_dir() and not filename.is_file(): if not filename.is_dir() and not filename.is_file():
logger.warning("Backtest file is missing skipping trades.") logger.warning("Backtest file is missing skipping trades.")
no_trades = True no_trades = True
try: try:
trades = load_trades( trades = load_trades(
config['trade_source'], config["trade_source"],
db_url=config.get('db_url'), db_url=config.get("db_url"),
exportfilename=filename, exportfilename=filename,
no_trades=no_trades, no_trades=no_trades,
strategy=config.get('strategy'), strategy=config.get("strategy"),
) )
except ValueError as e: except ValueError as e:
raise OperationalException(e) from e raise OperationalException(e) from e
if not trades.empty: 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, return {
"trades": trades, "ohlcv": data,
"pairs": pairs, "trades": trades,
"timerange": timerange, "pairs": pairs,
} "timerange": timerange,
}
def add_indicators(fig, row, indicators: Dict[str, Dict], data: pd.DataFrame) -> make_subplots: 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 :param data: candlestick DataFrame
""" """
plot_kinds = { plot_kinds = {
'scatter': go.Scatter, "scatter": go.Scatter,
'bar': go.Bar, "bar": go.Bar,
} }
for indicator, conf in indicators.items(): for indicator, conf in indicators.items():
logger.debug(f"indicator {indicator} with config {conf}") logger.debug(f"indicator {indicator} with config {conf}")
if indicator in data: if indicator in data:
kwargs = {'x': data['date'], kwargs = {"x": data["date"], "y": data[indicator].values, "name": indicator}
'y': data[indicator].values,
'name': indicator
}
plot_type = conf.get('type', 'scatter') plot_type = conf.get("type", "scatter")
color = conf.get('color') color = conf.get("color")
if plot_type == 'bar': if plot_type == "bar":
kwargs.update({'marker_color': color or 'DarkSlateGrey', kwargs.update(
'marker_line_color': color or 'DarkSlateGrey'}) {
"marker_color": color or "DarkSlateGrey",
"marker_line_color": color or "DarkSlateGrey",
}
)
else: else:
if color: if color:
kwargs.update({'line': {'color': color}}) kwargs.update({"line": {"color": color}})
kwargs['mode'] = 'lines' kwargs["mode"] = "lines"
if plot_type != 'scatter': if plot_type != "scatter":
logger.warning(f'Indicator {indicator} has unknown plot trace kind {plot_type}' logger.warning(
f', assuming "scatter".') 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) trace = plot_kinds[plot_type](**kwargs)
fig.add_trace(trace, row, 1) fig.add_trace(trace, row, 1)
else: else:
logger.info( logger.info(
'Indicator "%s" ignored. Reason: This indicator is not found ' 'Indicator "%s" ignored. Reason: This indicator is not found ' "in your strategy.",
'in your strategy.', indicator,
indicator
) )
return fig return fig
@ -168,33 +172,27 @@ def add_profit(fig, row, data: pd.DataFrame, column: str, name: str) -> make_sub
return fig return fig
def add_max_drawdown(fig, row, trades: pd.DataFrame, df_comb: pd.DataFrame, def add_max_drawdown(
timeframe: str, starting_balance: float) -> make_subplots: fig, row, trades: pd.DataFrame, df_comb: pd.DataFrame, timeframe: str, starting_balance: float
) -> make_subplots:
""" """
Add scatter points indicating max drawdown Add scatter points indicating max drawdown
""" """
try: try:
_, highdate, lowdate, _, _, max_drawdown = calculate_max_drawdown( _, highdate, lowdate, _, _, max_drawdown = calculate_max_drawdown(
trades, trades, starting_balance=starting_balance
starting_balance=starting_balance
) )
drawdown = go.Scatter( drawdown = go.Scatter(
x=[highdate, lowdate], x=[highdate, lowdate],
y=[ y=[
df_comb.loc[timeframe_to_prev_date(timeframe, highdate), 'cum_profit'], 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, lowdate), "cum_profit"],
], ],
mode='markers', mode="markers",
name=f"Max drawdown {max_drawdown:.2%}", name=f"Max drawdown {max_drawdown:.2%}",
text=f"Max drawdown {max_drawdown:.2%}", text=f"Max drawdown {max_drawdown:.2%}",
marker=dict( marker=dict(symbol="square-open", size=9, line=dict(width=2), color="green"),
symbol='square-open',
size=9,
line=dict(width=2),
color='green'
)
) )
fig.add_trace(drawdown, row, 1) fig.add_trace(drawdown, row, 1)
except ValueError: except ValueError:
@ -208,27 +206,25 @@ def add_underwater(fig, row, trades: pd.DataFrame, starting_balance: float) -> m
""" """
try: try:
underwater = calculate_underwater( underwater = calculate_underwater(
trades, trades, value_col="profit_abs", starting_balance=starting_balance
value_col="profit_abs",
starting_balance=starting_balance
) )
underwater_plot = go.Scatter( underwater_plot = go.Scatter(
x=underwater['date'], x=underwater["date"],
y=underwater['drawdown'], y=underwater["drawdown"],
name="Underwater Plot", name="Underwater Plot",
fill='tozeroy', fill="tozeroy",
fillcolor='#cc362b', fillcolor="#cc362b",
line={'color': '#cc362b'} line={"color": "#cc362b"},
) )
underwater_plot_relative = go.Scatter( underwater_plot_relative = go.Scatter(
x=underwater['date'], x=underwater["date"],
y=(-underwater['drawdown_relative']), y=(-underwater["drawdown_relative"]),
name="Underwater Plot (%)", name="Underwater Plot (%)",
fill='tozeroy', fill="tozeroy",
fillcolor='green', fillcolor="green",
line={'color': 'green'} line={"color": "green"},
) )
fig.add_trace(underwater_plot, row, 1) 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( drawdown = go.Scatter(
x=result.index, x=result.index,
y=result['open_trades'], y=result["open_trades"],
name="Parallel trades", name="Parallel trades",
fill='tozeroy', fill="tozeroy",
fillcolor='#242222', fillcolor="#242222",
line={'color': '#242222'}, line={"color": "#242222"},
) )
fig.add_trace(drawdown, row, 1) fig.add_trace(drawdown, row, 1)
except ValueError: except ValueError:
@ -266,52 +262,37 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
# Trades can be empty # Trades can be empty
if trades is not None and len(trades) > 0: if trades is not None and len(trades) > 0:
# Create description for exit summarizing the trade # Create description for exit summarizing the trade
trades['desc'] = trades.apply( trades["desc"] = trades.apply(
lambda row: f"{row['profit_ratio']:.2%}, " + lambda row: f"{row['profit_ratio']:.2%}, "
(f"{row['enter_tag']}, " if row['enter_tag'] is not None else "") + + (f"{row['enter_tag']}, " if row["enter_tag"] is not None else "")
f"{row['exit_reason']}, " + + f"{row['exit_reason']}, "
f"{row['trade_duration']} min", + f"{row['trade_duration']} min",
axis=1) axis=1,
)
trade_entries = go.Scatter( trade_entries = go.Scatter(
x=trades["open_date"], x=trades["open_date"],
y=trades["open_rate"], y=trades["open_rate"],
mode='markers', mode="markers",
name='Trade entry', name="Trade entry",
text=trades["desc"], text=trades["desc"],
marker=dict( marker=dict(symbol="circle-open", size=11, line=dict(width=2), color="cyan"),
symbol='circle-open',
size=11,
line=dict(width=2),
color='cyan'
)
) )
trade_exits = go.Scatter( trade_exits = go.Scatter(
x=trades.loc[trades['profit_ratio'] > 0, "close_date"], x=trades.loc[trades["profit_ratio"] > 0, "close_date"],
y=trades.loc[trades['profit_ratio'] > 0, "close_rate"], y=trades.loc[trades["profit_ratio"] > 0, "close_rate"],
text=trades.loc[trades['profit_ratio'] > 0, "desc"], text=trades.loc[trades["profit_ratio"] > 0, "desc"],
mode='markers', mode="markers",
name='Exit - Profit', name="Exit - Profit",
marker=dict( marker=dict(symbol="square-open", size=11, line=dict(width=2), color="green"),
symbol='square-open',
size=11,
line=dict(width=2),
color='green'
)
) )
trade_exits_loss = go.Scatter( trade_exits_loss = go.Scatter(
x=trades.loc[trades['profit_ratio'] <= 0, "close_date"], x=trades.loc[trades["profit_ratio"] <= 0, "close_date"],
y=trades.loc[trades['profit_ratio'] <= 0, "close_rate"], y=trades.loc[trades["profit_ratio"] <= 0, "close_rate"],
text=trades.loc[trades['profit_ratio'] <= 0, "desc"], text=trades.loc[trades["profit_ratio"] <= 0, "desc"],
mode='markers', mode="markers",
name='Exit - Loss', name="Exit - Loss",
marker=dict( marker=dict(symbol="square-open", size=11, line=dict(width=2), color="red"),
symbol='square-open',
size=11,
line=dict(width=2),
color='red'
)
) )
fig.add_trace(trade_entries, 1, 1) fig.add_trace(trade_entries, 1, 1)
fig.add_trace(trade_exits, 1, 1) fig.add_trace(trade_exits, 1, 1)
@ -321,8 +302,9 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
return fig return fig
def create_plotconfig(indicators1: List[str], indicators2: List[str], def create_plotconfig(
plot_config: Dict[str, Dict]) -> Dict[str, Dict]: 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 Combines indicators 1 and indicators 2 into plot_config if necessary
:param indicators1: List containing Main plot indicators :param indicators1: List containing Main plot indicators
@ -333,34 +315,40 @@ def create_plotconfig(indicators1: List[str], indicators2: List[str],
if plot_config: if plot_config:
if indicators1: if indicators1:
plot_config['main_plot'] = {ind: {} for ind in indicators1} plot_config["main_plot"] = {ind: {} for ind in indicators1}
if indicators2: 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 not plot_config:
# If no indicators and no plot-config given, use defaults. # If no indicators and no plot-config given, use defaults.
if not indicators1: if not indicators1:
indicators1 = ['sma', 'ema3', 'ema5'] indicators1 = ["sma", "ema3", "ema5"]
if not indicators2: if not indicators2:
indicators2 = ['macd', 'macdsignal'] indicators2 = ["macd", "macdsignal"]
# Create subplot configuration if plot_config is not available. # Create subplot configuration if plot_config is not available.
plot_config = { plot_config = {
'main_plot': {ind: {} for ind in indicators1}, "main_plot": {ind: {} for ind in indicators1},
'subplots': {'Other': {ind: {} for ind in indicators2}}, "subplots": {"Other": {ind: {} for ind in indicators2}},
} }
if 'main_plot' not in plot_config: if "main_plot" not in plot_config:
plot_config['main_plot'] = {} plot_config["main_plot"] = {}
if 'subplots' not in plot_config: if "subplots" not in plot_config:
plot_config['subplots'] = {} plot_config["subplots"] = {}
return plot_config return plot_config
def plot_area(fig, row: int, data: pd.DataFrame, indicator_a: str, def plot_area(
indicator_b: str, label: str = "", fig,
fill_color: str = "rgba(0,176,246,0.2)") -> make_subplots: row: int,
""" Creates a plot for the area between two traces and adds it to fig. 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 fig: Plot figure to append to
:param row: row number for this plot :param row: row number for this plot
:param data: candlestick DataFrame :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: if indicator_a in data and indicator_b in data:
# make lines invisible to get the area plotted, only. # 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 # TODO: Figure out why scattergl causes problems plotly/plotly.js#2284
trace_a = go.Scatter(x=data.date, y=data[indicator_a], trace_a = go.Scatter(x=data.date, y=data[indicator_a], showlegend=False, line=line)
showlegend=False, trace_b = go.Scatter(
line=line) x=data.date,
trace_b = go.Scatter(x=data.date, y=data[indicator_b], name=label, y=data[indicator_b],
fill="tonexty", fillcolor=fill_color, name=label,
line=line) fill="tonexty",
fillcolor=fill_color,
line=line,
)
fig.add_trace(trace_a, row, 1) fig.add_trace(trace_a, row, 1)
fig.add_trace(trace_b, row, 1) fig.add_trace(trace_b, row, 1)
return fig return fig
def add_areas(fig, row: int, data: pd.DataFrame, indicators) -> make_subplots: 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 fig: Plot figure to append to
:param row: row number for this plot :param row: row number for this plot
:param data: candlestick DataFrame :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 :return: fig with added filled_traces plot
""" """
for indicator, ind_conf in indicators.items(): for indicator, ind_conf in indicators.items():
if 'fill_to' in ind_conf: if "fill_to" in ind_conf:
indicator_b = ind_conf['fill_to'] indicator_b = ind_conf["fill_to"]
if indicator in data and indicator_b in data: if indicator in data and indicator_b in data:
label = ind_conf.get('fill_label', label = ind_conf.get("fill_label", f"{indicator}<>{indicator_b}")
f'{indicator}<>{indicator_b}') fill_color = ind_conf.get("fill_color", "rgba(0,176,246,0.2)")
fill_color = ind_conf.get('fill_color', 'rgba(0,176,246,0.2)') fig = plot_area(
fig = plot_area(fig, row, data, indicator, indicator_b, fig, row, data, indicator, indicator_b, label=label, fill_color=fill_color
label=label, fill_color=fill_color) )
elif indicator not in data: elif indicator not in data:
logger.info( logger.info(
'Indicator "%s" ignored. Reason: This indicator is not ' 'Indicator "%s" ignored. Reason: This indicator is not '
'found in your strategy.', indicator "found in your strategy.",
indicator,
) )
elif indicator_b not in data: elif indicator_b not in data:
logger.info( logger.info(
'fill_to: "%s" ignored. Reason: This indicator is not ' 'fill_to: "%s" ignored. Reason: This indicator is not ' "in your strategy.",
'in your strategy.', indicator_b indicator_b,
) )
return fig return fig
def create_scatter( def create_scatter(data, column_name, color, direction) -> Optional[go.Scatter]:
data,
column_name,
color,
direction
) -> Optional[go.Scatter]:
if column_name in data.columns: if column_name in data.columns:
df_short = data[data[column_name] == 1] df_short = data[data[column_name] == 1]
if len(df_short) > 0: if len(df_short) > 0:
shorts = go.Scatter( shorts = go.Scatter(
x=df_short.date, x=df_short.date,
y=df_short.close, y=df_short.close,
mode='markers', mode="markers",
name=column_name, name=column_name,
marker=dict( marker=dict(
symbol=f"triangle-{direction}-dot", symbol=f"triangle-{direction}-dot",
size=9, size=9,
line=dict(width=1), line=dict(width=1),
color=color, color=color,
) ),
) )
return shorts return shorts
else: else:
@ -446,10 +432,14 @@ def create_scatter(
def generate_candlestick_graph( def generate_candlestick_graph(
pair: str, data: pd.DataFrame, trades: Optional[pd.DataFrame] = None, *, pair: str,
indicators1: Optional[List[str]] = None, indicators2: Optional[List[str]] = None, data: pd.DataFrame,
plot_config: Optional[Dict[str, Dict]] = None, trades: Optional[pd.DataFrame] = None,
) -> go.Figure: *,
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 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 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 [], indicators2 or [],
plot_config or {}, plot_config or {},
) )
rows = 2 + len(plot_config['subplots']) rows = 2 + len(plot_config["subplots"])
row_widths = [1 for _ in plot_config['subplots']] row_widths = [1 for _ in plot_config["subplots"]]
# Define the graph # Define the graph
fig = make_subplots( fig = make_subplots(
rows=rows, rows=rows,
@ -476,127 +466,131 @@ def generate_candlestick_graph(
row_width=row_widths + [1, 4], row_width=row_widths + [1, 4],
vertical_spacing=0.0001, vertical_spacing=0.0001,
) )
fig['layout'].update(title=pair) fig["layout"].update(title=pair)
fig['layout']['yaxis1'].update(title='Price') fig["layout"]["yaxis1"].update(title="Price")
fig['layout']['yaxis2'].update(title='Volume') fig["layout"]["yaxis2"].update(title="Volume")
for i, name in enumerate(plot_config['subplots']): for i, name in enumerate(plot_config["subplots"]):
fig['layout'][f'yaxis{3 + i}'].update(title=name) fig["layout"][f"yaxis{3 + i}"].update(title=name)
fig['layout']['xaxis']['rangeslider'].update(visible=False) fig["layout"]["xaxis"]["rangeslider"].update(visible=False)
fig.update_layout(modebar_add=["v1hovermode", "toggleSpikeLines"]) fig.update_layout(modebar_add=["v1hovermode", "toggleSpikeLines"])
# Common information # Common information
candles = go.Candlestick( candles = go.Candlestick(
x=data.date, x=data.date, open=data.open, high=data.high, low=data.low, close=data.close, name="Price"
open=data.open,
high=data.high,
low=data.low,
close=data.close,
name='Price'
) )
fig.add_trace(candles, 1, 1) fig.add_trace(candles, 1, 1)
longs = create_scatter(data, 'enter_long', 'green', 'up') longs = create_scatter(data, "enter_long", "green", "up")
exit_longs = create_scatter(data, 'exit_long', 'red', 'down') exit_longs = create_scatter(data, "exit_long", "red", "down")
shorts = create_scatter(data, 'enter_short', 'blue', 'down') shorts = create_scatter(data, "enter_short", "blue", "down")
exit_shorts = create_scatter(data, 'exit_short', 'violet', 'up') exit_shorts = create_scatter(data, "exit_short", "violet", "up")
for scatter in [longs, exit_longs, shorts, exit_shorts]: for scatter in [longs, exit_longs, shorts, exit_shorts]:
if scatter: if scatter:
fig.add_trace(scatter, 1, 1) fig.add_trace(scatter, 1, 1)
# Add Bollinger Bands # Add Bollinger Bands
fig = plot_area(fig, 1, data, 'bb_lowerband', 'bb_upperband', fig = plot_area(fig, 1, data, "bb_lowerband", "bb_upperband", label="Bollinger Band")
label="Bollinger Band")
# prevent bb_lower and bb_upper from plotting # prevent bb_lower and bb_upper from plotting
try: try:
del plot_config['main_plot']['bb_lowerband'] del plot_config["main_plot"]["bb_lowerband"]
del plot_config['main_plot']['bb_upperband'] del plot_config["main_plot"]["bb_upperband"]
except KeyError: except KeyError:
pass pass
# main plot goes to row 1 # main plot goes to row 1
fig = add_indicators(fig=fig, row=1, indicators=plot_config['main_plot'], data=data) 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_areas(fig, 1, data, plot_config["main_plot"])
fig = plot_trades(fig, trades) fig = plot_trades(fig, trades)
# sub plot: Volume goes to row 2 # sub plot: Volume goes to row 2
volume = go.Bar( volume = go.Bar(
x=data['date'], x=data["date"],
y=data['volume'], y=data["volume"],
name='Volume', name="Volume",
marker_color='DarkSlateGrey', marker_color="DarkSlateGrey",
marker_line_color='DarkSlateGrey' marker_line_color="DarkSlateGrey",
) )
fig.add_trace(volume, 2, 1) fig.add_trace(volume, 2, 1)
# add each sub plot to a separate row # add each sub plot to a separate row
for i, label in enumerate(plot_config['subplots']): for i, label in enumerate(plot_config["subplots"]):
sub_config = plot_config['subplots'][label] sub_config = plot_config["subplots"][label]
row = 3 + i row = 3 + i
fig = add_indicators(fig=fig, row=row, indicators=sub_config, fig = add_indicators(fig=fig, row=row, indicators=sub_config, data=data)
data=data)
# fill area between indicators ( 'fill_to': 'other_indicator') # fill area between indicators ( 'fill_to': 'other_indicator')
fig = add_areas(fig, row, data, sub_config) fig = add_areas(fig, row, data, sub_config)
return fig return fig
def generate_profit_graph(pairs: str, data: Dict[str, pd.DataFrame], def generate_profit_graph(
trades: pd.DataFrame, timeframe: str, stake_currency: str, pairs: str,
starting_balance: float) -> go.Figure: 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" # Combine close-values for all pairs, rename columns to "pair"
try: try:
df_comb = combine_dataframes_with_mean(data, "close") df_comb = combine_dataframes_with_mean(data, "close")
except ValueError: except ValueError:
raise OperationalException( raise OperationalException(
"No data found. Please make sure that data is available for " "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 # Trim trades to available OHLCV data
trades = extract_trades_of_period(df_comb, trades, date_index=True) trades = extract_trades_of_period(df_comb, trades, date_index=True)
if len(trades) == 0: if len(trades) == 0:
raise OperationalException('No trades found in selected timerange.') raise OperationalException("No trades found in selected timerange.")
# Add combined cumulative profit # 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 # Plot the pairs average close prices, and total profit growth
avgclose = go.Scatter( avgclose = go.Scatter(
x=df_comb.index, x=df_comb.index,
y=df_comb['mean'], y=df_comb["mean"],
name='Avg close price', name="Avg close price",
) )
fig = make_subplots(rows=6, cols=1, shared_xaxes=True, fig = make_subplots(
row_heights=[1, 1, 1, 0.5, 0.75, 0.75], rows=6,
vertical_spacing=0.05, cols=1,
subplot_titles=[ shared_xaxes=True,
"AVG Close Price", row_heights=[1, 1, 1, 0.5, 0.75, 0.75],
"Combined Profit", vertical_spacing=0.05,
"Profit per pair", subplot_titles=[
"Parallelism", "AVG Close Price",
"Underwater", "Combined Profit",
"Relative Drawdown", "Profit per pair",
]) "Parallelism",
fig['layout'].update(title="Freqtrade Profit plot") "Underwater",
fig['layout']['yaxis1'].update(title='Price') "Relative Drawdown",
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"].update(title="Freqtrade Profit plot")
fig['layout']['yaxis5'].update(title='Underwater Plot') fig["layout"]["yaxis1"].update(title="Price")
fig['layout']['yaxis6'].update(title='Underwater Plot Relative (%)', tickformat=',.2%') fig["layout"]["yaxis2"].update(title=f"Profit {stake_currency}")
fig['layout']['xaxis']['rangeslider'].update(visible=False) 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.update_layout(modebar_add=["v1hovermode", "toggleSpikeLines"])
fig.add_trace(avgclose, 1, 1) 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_max_drawdown(fig, 2, trades, df_comb, timeframe, starting_balance)
fig = add_parallelism(fig, 4, trades, timeframe) fig = add_parallelism(fig, 4, trades, timeframe)
# Two rows consumed # Two rows consumed
fig = add_underwater(fig, 5, trades, starting_balance) fig = add_underwater(fig, 5, trades, starting_balance)
for pair in pairs: for pair in pairs:
profit_col = f'cum_profit_{pair}' profit_col = f"cum_profit_{pair}"
try: try:
df_comb = create_cum_profit(df_comb, trades[trades['pair'] == pair], profit_col, df_comb = create_cum_profit(
timeframe) df_comb, trades[trades["pair"] == pair], profit_col, timeframe
)
fig = add_profit(fig, 3, df_comb, profit_col, f"Profit {pair}") fig = add_profit(fig, 3, df_comb, profit_col, f"Profit {pair}")
except ValueError: except ValueError:
pass 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 Generate filenames per pair/timeframe to be used for storing plots
""" """
pair_s = pair_to_filename(pair) 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 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) directory.mkdir(parents=True, exist_ok=True)
_filename = directory.joinpath(filename) _filename = directory.joinpath(filename)
plot(fig, filename=str(_filename), plot(fig, filename=str(_filename), auto_open=auto_open)
auto_open=auto_open)
logger.info(f"Stored plot as {_filename}") logger.info(f"Stored plot as {_filename}")
@ -650,17 +643,17 @@ def load_and_plot_trades(config: Config):
strategy.ft_bot_start() strategy.ft_bot_start()
strategy_safe_wrapper(strategy.bot_loop_start)(current_time=datetime.now(timezone.utc)) 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) plot_elements = init_plotscript(config, list(exchange.markets), strategy.startup_candle_count)
timerange = plot_elements['timerange'] timerange = plot_elements["timerange"]
trades = plot_elements['trades'] trades = plot_elements["trades"]
pair_counter = 0 pair_counter = 0
for pair, data in plot_elements["ohlcv"].items(): for pair, data in plot_elements["ohlcv"].items():
pair_counter += 1 pair_counter += 1
logger.info("analyse pair %s", pair) 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) df_analyzed = trim_dataframe(df_analyzed, timerange)
if not trades.empty: 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) trades_pair = extract_trades_of_period(df_analyzed, trades_pair)
else: else:
trades_pair = trades trades_pair = trades
@ -669,15 +662,18 @@ def load_and_plot_trades(config: Config):
pair=pair, pair=pair,
data=df_analyzed, data=df_analyzed,
trades=trades_pair, trades=trades_pair,
indicators1=config.get('indicators1', []), indicators1=config.get("indicators1", []),
indicators2=config.get('indicators2', []), indicators2=config.get("indicators2", []),
plot_config=strategy.plot_config if hasattr(strategy, 'plot_config') else {} plot_config=strategy.plot_config if hasattr(strategy, "plot_config") else {},
) )
store_plot_file(fig, filename=generate_plot_filename(pair, config['timeframe']), store_plot_file(
directory=config['user_data_dir'] / 'plot') 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: def plot_profit(config: Config) -> None:
@ -687,28 +683,37 @@ def plot_profit(config: Config) -> None:
But should be somewhat proportional, and therefore useful But should be somewhat proportional, and therefore useful
in helping out to find a good algorithm. in helping out to find a good algorithm.
""" """
if 'timeframe' not in config: if "timeframe" not in config:
raise OperationalException('Timeframe must be set in either config or via --timeframe.') raise OperationalException("Timeframe must be set in either config or via --timeframe.")
exchange = ExchangeResolver.load_exchange(config) exchange = ExchangeResolver.load_exchange(config)
plot_elements = init_plotscript(config, list(exchange.markets)) plot_elements = init_plotscript(config, list(exchange.markets))
trades = plot_elements['trades'] trades = plot_elements["trades"]
# Filter trades to relevant pairs # Filter trades to relevant pairs
# Remove open pairs - we don't know the profit yet so can't calculate profit for these. # 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. # Also, If only one open pair is left, then the profit-generation would fail.
trades = trades[(trades['pair'].isin(plot_elements['pairs'])) trades = trades[
& (~trades['close_date'].isnull()) (trades["pair"].isin(plot_elements["pairs"])) & (~trades["close_date"].isnull())
] ]
if len(trades) == 0: if len(trades) == 0:
raise OperationalException("No trades found, cannot generate Profit-plot without " raise OperationalException(
"trades from either Backtest result or database.") "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. # Create an average close price of all the pairs that were involved.
# this could be useful to gauge the overall market trend # this could be useful to gauge the overall market trend
fig = generate_profit_graph(plot_elements['pairs'], plot_elements['ohlcv'], fig = generate_profit_graph(
trades, config['timeframe'], plot_elements["pairs"],
config.get('stake_currency', ''), plot_elements["ohlcv"],
config.get('available_capital', config['dry_run_wallet'])) trades,
store_plot_file(fig, filename='freqtrade-profit-plot.html', config["timeframe"],
directory=config['user_data_dir'] / 'plot', config.get("stake_currency", ""),
auto_open=config.get('plot_auto_open', False)) 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),
)

View File

@ -1,5 +1,5 @@
# pragma pylint: disable=W0603 # pragma pylint: disable=W0603
""" Wallet """ """Wallet"""
import logging import logging
from copy import deepcopy from copy import deepcopy
@ -31,18 +31,17 @@ class PositionWallet(NamedTuple):
position: float = 0 position: float = 0
leverage: float = 0 leverage: float = 0
collateral: float = 0 collateral: float = 0
side: str = 'long' side: str = "long"
class Wallets: class Wallets:
def __init__(self, config: Config, exchange: Exchange, is_backtest: bool = False) -> None: def __init__(self, config: Config, exchange: Exchange, is_backtest: bool = False) -> None:
self._config = config self._config = config
self._is_backtest = is_backtest self._is_backtest = is_backtest
self._exchange = exchange self._exchange = exchange
self._wallets: Dict[str, Wallet] = {} self._wallets: Dict[str, Wallet] = {}
self._positions: Dict[str, PositionWallet] = {} 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._last_wallet_refresh: Optional[datetime] = None
self.update() self.update()
@ -88,17 +87,12 @@ class Wallets:
tot_in_trades = sum(trade.stake_amount for trade in open_trades) tot_in_trades = sum(trade.stake_amount for trade in open_trades)
used_stake = 0.0 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 current_stake = self.start_cap + tot_profit - tot_in_trades
total_stake = current_stake total_stake = current_stake
for trade in open_trades: for trade in open_trades:
curr = self._exchange.get_pair_base_currency(trade.pair) curr = self._exchange.get_pair_base_currency(trade.pair)
_wallets[curr] = Wallet( _wallets[curr] = Wallet(curr, trade.amount, 0, trade.amount)
curr,
trade.amount,
0,
trade.amount
)
else: else:
tot_in_trades = 0 tot_in_trades = 0
for position in open_trades: for position in open_trades:
@ -108,20 +102,21 @@ class Wallets:
leverage = position.leverage leverage = position.leverage
tot_in_trades += collateral tot_in_trades += collateral
_positions[position.pair] = PositionWallet( _positions[position.pair] = PositionWallet(
position.pair, position=size, position.pair,
position=size,
leverage=leverage, leverage=leverage,
collateral=collateral, collateral=collateral,
side=position.trade_direction side=position.trade_direction,
) )
current_stake = self.start_cap + tot_profit - tot_in_trades current_stake = self.start_cap + tot_profit - tot_in_trades
used_stake = tot_in_trades used_stake = tot_in_trades
total_stake = current_stake + tot_in_trades total_stake = current_stake + tot_in_trades
_wallets[self._config['stake_currency']] = Wallet( _wallets[self._config["stake_currency"]] = Wallet(
currency=self._config['stake_currency'], currency=self._config["stake_currency"],
free=current_stake, free=current_stake,
used=used_stake, used=used_stake,
total=total_stake total=total_stake,
) )
self._wallets = _wallets self._wallets = _wallets
self._positions = _positions self._positions = _positions
@ -133,9 +128,9 @@ class Wallets:
if isinstance(balances[currency], dict): if isinstance(balances[currency], dict):
self._wallets[currency] = Wallet( self._wallets[currency] = Wallet(
currency, currency,
balances[currency].get('free'), balances[currency].get("free"),
balances[currency].get('used'), balances[currency].get("used"),
balances[currency].get('total') balances[currency].get("total"),
) )
# Remove currencies no longer in get_balances output # Remove currencies no longer in get_balances output
for currency in deepcopy(self._wallets): for currency in deepcopy(self._wallets):
@ -145,18 +140,19 @@ class Wallets:
positions = self._exchange.fetch_positions() positions = self._exchange.fetch_positions()
self._positions = {} self._positions = {}
for position in positions: for position in positions:
symbol = position['symbol'] symbol = position["symbol"]
if position['side'] is None or position['collateral'] == 0.0: if position["side"] is None or position["collateral"] == 0.0:
# Position is not open ... # Position is not open ...
continue continue
size = self._exchange._contracts_to_amount(symbol, position['contracts']) size = self._exchange._contracts_to_amount(symbol, position["contracts"])
collateral = safe_value_fallback(position, 'collateral', 'initialMargin', 0.0) collateral = safe_value_fallback(position, "collateral", "initialMargin", 0.0)
leverage = position['leverage'] leverage = position["leverage"]
self._positions[symbol] = PositionWallet( self._positions[symbol] = PositionWallet(
symbol, position=size, symbol,
position=size,
leverage=leverage, leverage=leverage,
collateral=collateral, collateral=collateral,
side=position['side'] side=position["side"],
) )
def update(self, require_update: bool = True) -> None: 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 is None
or (self._last_wallet_refresh + timedelta(seconds=3600) < now) 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() self._update_live()
else: else:
self._update_dry() self._update_dry()
if not self._is_backtest: if not self._is_backtest:
logger.info('Wallets synced.') logger.info("Wallets synced.")
self._last_wallet_refresh = dt_now() self._last_wallet_refresh = dt_now()
def get_all_balances(self) -> Dict[str, Wallet]: def get_all_balances(self) -> Dict[str, Wallet]:
@ -222,11 +218,11 @@ class Wallets:
or by using current balance subtracting or by using current balance subtracting
""" """
if "available_capital" in self._config: if "available_capital" in self._config:
return self._config['available_capital'] return self._config["available_capital"]
else: else:
tot_profit = Trade.get_total_closed_profit() tot_profit = Trade.get_total_closed_profit()
open_stakes = Trade.total_open_trades_stakes() 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 return available_balance - tot_profit + open_stakes
def get_total_stake_amount(self): def get_total_stake_amount(self):
@ -238,7 +234,7 @@ class Wallets:
""" """
val_tied_up = Trade.total_open_trades_stakes() val_tied_up = Trade.total_open_trades_stakes()
if "available_capital" in self._config: 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() tot_profit = Trade.get_total_closed_profit()
available_amount = starting_balance + tot_profit available_amount = starting_balance + tot_profit
@ -246,8 +242,9 @@ class Wallets:
# Ensure <tradable_balance_ratio>% is used from the overall balance # Ensure <tradable_balance_ratio>% is used from the overall balance
# Otherwise we'd risk lowering stakes with each open trade. # Otherwise we'd risk lowering stakes with each open trade.
# (tied up + current free) * ratio) - tied up # (tied up + current free) * ratio) - tied up
available_amount = ((val_tied_up + self.get_free(self._config['stake_currency'])) * available_amount = (
self._config['tradable_balance_ratio']) val_tied_up + self.get_free(self._config["stake_currency"])
) * self._config["tradable_balance_ratio"]
return available_amount return available_amount
def get_available_stake_amount(self) -> float: def get_available_stake_amount(self) -> float:
@ -258,11 +255,12 @@ class Wallets:
(<open_trade stakes> + free amount) * tradable_balance_ratio - <open_trade stakes> (<open_trade stakes> + free amount) * tradable_balance_ratio - <open_trade stakes>
""" """
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) return min(self.get_total_stake_amount() - Trade.total_open_trades_stakes(), free)
def _calculate_unlimited_stake_amount(self, available_amount: float, def _calculate_unlimited_stake_amount(
val_tied_up: float, max_open_trades: IntOrInf) -> float: self, available_amount: float, val_tied_up: float, max_open_trades: IntOrInf
) -> float:
""" """
Calculate stake amount for "unlimited" stake amount Calculate stake amount for "unlimited" stake amount
:return: 0 if max number of trades reached, else stake_amount to use. :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 :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 # Remaining amount needs to be at least stake_amount * last_stake_amount_min_ratio
# Otherwise the remaining amount is too low to trade. # 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) stake_amount = min(stake_amount, available_amount)
else: else:
stake_amount = 0 stake_amount = 0
@ -299,7 +297,8 @@ class Wallets:
return stake_amount return stake_amount
def get_trade_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 Calculate stake amount for the trade
:return: float: Stake amount :return: float: Stake amount
@ -315,21 +314,27 @@ class Wallets:
if edge: if edge:
stake_amount = edge.stake_amount( stake_amount = edge.stake_amount(
pair, pair,
self.get_free(self._config['stake_currency']), self.get_free(self._config["stake_currency"]),
self.get_total(self._config['stake_currency']), self.get_total(self._config["stake_currency"]),
val_tied_up val_tied_up,
) )
else: else:
stake_amount = self._config['stake_amount'] stake_amount = self._config["stake_amount"]
if stake_amount == UNLIMITED_STAKE_AMOUNT: if stake_amount == UNLIMITED_STAKE_AMOUNT:
stake_amount = self._calculate_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) return self._check_available_stake_amount(stake_amount, available_amount)
def validate_stake_amount(self, pair: str, stake_amount: Optional[float], def validate_stake_amount(
min_stake_amount: Optional[float], max_stake_amount: float, self,
trade_amount: Optional[float]): pair: str,
stake_amount: Optional[float],
min_stake_amount: Optional[float],
max_stake_amount: float,
trade_amount: Optional[float],
):
if not stake_amount: if not stake_amount:
logger.debug(f"Stake amount is {stake_amount}, ignoring possible trade for {pair}.") logger.debug(f"Stake amount is {stake_amount}, ignoring possible trade for {pair}.")
return 0 return 0
@ -342,8 +347,10 @@ class Wallets:
if min_stake_amount is not None and min_stake_amount > max_allowed_stake: if min_stake_amount is not None and min_stake_amount > max_allowed_stake:
if not self._is_backtest: if not self._is_backtest:
logger.warning("Minimum stake amount > available balance. " logger.warning(
f"{min_stake_amount} > {max_allowed_stake}") "Minimum stake amount > available balance. "
f"{min_stake_amount} > {max_allowed_stake}"
)
return 0 return 0
if min_stake_amount is not None and stake_amount < min_stake_amount: if min_stake_amount is not None and stake_amount < min_stake_amount:
if not self._is_backtest: if not self._is_backtest:

View File

@ -1,6 +1,7 @@
""" """
Main Freqtrade worker class. Main Freqtrade worker class.
""" """
import logging import logging
import time import time
import traceback import traceback
@ -52,13 +53,15 @@ class Worker:
# Init the instance of the bot # Init the instance of the bot
self.freqtrade = FreqtradeBot(self._config) self.freqtrade = FreqtradeBot(self._config)
internals_config = self._config.get('internals', {}) internals_config = self._config.get("internals", {})
self._throttle_secs = internals_config.get('process_throttle_secs', self._throttle_secs = internals_config.get("process_throttle_secs", PROCESS_THROTTLE_SECS)
PROCESS_THROTTLE_SECS) self._heartbeat_interval = internals_config.get("heartbeat_interval", 60)
self._heartbeat_interval = internals_config.get('heartbeat_interval', 60)
self._sd_notify = sdnotify.SystemdNotifier() if \ self._sd_notify = (
self._config.get('internals', {}).get('sd_notify', False) else None sdnotify.SystemdNotifier()
if self._config.get("internals", {}).get("sd_notify", False)
else None
)
def _notify(self, message: str) -> None: def _notify(self, message: str) -> None:
""" """
@ -86,12 +89,12 @@ class Worker:
# Log state transition # Log state transition
if state != old_state: if state != old_state:
if old_state != State.RELOAD_CONFIG: if old_state != State.RELOAD_CONFIG:
self.freqtrade.notify_status(f'{state.name.lower()}') self.freqtrade.notify_status(f"{state.name.lower()}")
logger.info( 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: if state == State.RUNNING:
self.freqtrade.startup() self.freqtrade.startup()
@ -113,26 +116,36 @@ class Worker:
self._notify("WATCHDOG=1\nSTATUS=State: RUNNING.") self._notify("WATCHDOG=1\nSTATUS=State: RUNNING.")
# Use an offset of 1s to ensure a new candle has been issued # Use an offset of 1s to ensure a new candle has been issued
self._throttle(func=self._process_running, throttle_secs=self._throttle_secs, self._throttle(
timeframe=self._config['timeframe'] if self._config else None, func=self._process_running,
timeframe_offset=1) throttle_secs=self._throttle_secs,
timeframe=self._config["timeframe"] if self._config else None,
timeframe_offset=1,
)
if self._heartbeat_interval: if self._heartbeat_interval:
now = time.time() now = time.time()
if (now - self._heartbeat_msg) > self._heartbeat_interval: if (now - self._heartbeat_msg) > self._heartbeat_interval:
version = __version__ version = __version__
strategy_version = self.freqtrade.strategy.version() strategy_version = self.freqtrade.strategy.version()
if (strategy_version is not None): if strategy_version is not None:
version += ', strategy_version: ' + strategy_version version += ", strategy_version: " + strategy_version
logger.info(f"Bot heartbeat. PID={getpid()}, " logger.info(
f"version='{version}', state='{state.name}'") f"Bot heartbeat. PID={getpid()}, " f"version='{version}', state='{state.name}'"
)
self._heartbeat_msg = now self._heartbeat_msg = now
return state return state
def _throttle(self, func: Callable[..., Any], throttle_secs: float, def _throttle(
timeframe: Optional[str] = None, timeframe_offset: float = 1.0, self,
*args, **kwargs) -> Any: func: Callable[..., Any],
throttle_secs: float,
timeframe: Optional[str] = None,
timeframe_offset: float = 1.0,
*args,
**kwargs,
) -> Any:
""" """
Throttles the given callable that it Throttles the given callable that it
takes at least `min_secs` to finish execution. takes at least `min_secs` to finish execution.
@ -160,10 +173,11 @@ class Worker:
sleep_duration = max(sleep_duration, 0.0) sleep_duration = max(sleep_duration, 0.0)
# next_iter = datetime.now(timezone.utc) + timedelta(seconds=sleep_duration) # next_iter = datetime.now(timezone.utc) + timedelta(seconds=sleep_duration)
logger.debug(f"Throttling with '{func.__name__}()': sleep for {sleep_duration:.2f} s, " logger.debug(
f"last iteration took {time_passed:.2f} s." f"Throttling with '{func.__name__}()': sleep for {sleep_duration:.2f} s, "
# f"next: {next_iter}" f"last iteration took {time_passed:.2f} s."
) # f"next: {next_iter}"
)
self._sleep(sleep_duration) self._sleep(sleep_duration)
return result return result
@ -183,14 +197,13 @@ class Worker:
time.sleep(RETRY_TIMEOUT) time.sleep(RETRY_TIMEOUT)
except OperationalException: except OperationalException:
tb = traceback.format_exc() 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( self.freqtrade.notify_status(
f'*OperationalException:*\n```\n{tb}```\n {hint}', f"*OperationalException:*\n```\n{tb}```\n {hint}", msg_type=RPCMessageType.EXCEPTION
msg_type=RPCMessageType.EXCEPTION
) )
logger.exception('OperationalException. Stopping trader ...') logger.exception("OperationalException. Stopping trader ...")
self.freqtrade.state = State.STOPPED self.freqtrade.state = State.STOPPED
def _reconfigure(self) -> None: def _reconfigure(self) -> None:
@ -207,7 +220,7 @@ class Worker:
# Load and validate config and create new instance of the bot # Load and validate config and create new instance of the bot
self._init(True) self._init(True)
self.freqtrade.notify_status('config reloaded') self.freqtrade.notify_status("config reloaded")
# Tell systemd that we completed reconfiguration # Tell systemd that we completed reconfiguration
self._notify("READY=1") self._notify("READY=1")
@ -217,5 +230,5 @@ class Worker:
self._notify("STOPPING=1") self._notify("STOPPING=1")
if self.freqtrade: if self.freqtrade:
self.freqtrade.notify_status('process died') self.freqtrade.notify_status("process died")
self.freqtrade.cleanup() self.freqtrade.cleanup()