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
"""
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

View File

@ -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),
)

View File

@ -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 <tradable_balance_ratio>% 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:
(<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)
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:

View File

@ -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()