mirror of
https://github.com/freqtrade/freqtrade.git
synced 2024-11-10 10:21:59 +00:00
ruff format: more files
This commit is contained in:
parent
f8f9ac38b2
commit
73e182260e
|
@ -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]]:
|
||||
|
@ -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
|
||||
|
|
|
@ -50,51 +50,53 @@ 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,
|
||||
return {
|
||||
"ohlcv": data,
|
||||
"trades": trades,
|
||||
"pairs": pairs,
|
||||
"timerange": timerange,
|
||||
|
@ -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,33 +315,39 @@ 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:
|
||||
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
|
||||
|
@ -372,14 +360,17 @@ 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
|
||||
|
@ -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,8 +432,12 @@ 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,
|
||||
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:
|
||||
"""
|
||||
|
@ -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,95 +466,97 @@ 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,
|
||||
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=[
|
||||
|
@ -574,29 +566,31 @@ def generate_profit_graph(pairs: str, data: Dict[str, pd.DataFrame],
|
|||
"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["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),
|
||||
)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,7 +173,8 @@ 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, "
|
||||
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}"
|
||||
)
|
||||
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue
Block a user