Merge remote-tracking branch 'origin/tmp/calcprofit' into pr/mkavinkumar1/6545
This commit is contained in:
@@ -6,6 +6,7 @@ Contains all start-commands, subcommands and CLI Interface creation.
|
||||
Note: Be careful with file-scoped imports in these subfiles.
|
||||
as they are parsed on startup, nothing containing optional modules should be loaded.
|
||||
"""
|
||||
from freqtrade.commands.analyze_commands import start_analysis_entries_exits
|
||||
from freqtrade.commands.arguments import Arguments
|
||||
from freqtrade.commands.build_config_commands import start_new_config
|
||||
from freqtrade.commands.data_commands import (start_convert_data, start_convert_trades,
|
||||
|
||||
69
freqtrade/commands/analyze_commands.py
Executable file
69
freqtrade/commands/analyze_commands.py
Executable file
@@ -0,0 +1,69 @@
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.enums import RunMode
|
||||
from freqtrade.exceptions import OperationalException
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_analyze_configuration(args: Dict[str, Any], method: RunMode) -> Dict[str, Any]:
|
||||
"""
|
||||
Prepare the configuration for the entry/exit reason analysis module
|
||||
:param args: Cli args from Arguments()
|
||||
:param method: Bot running mode
|
||||
:return: Configuration
|
||||
"""
|
||||
config = setup_utils_configuration(args, method)
|
||||
|
||||
no_unlimited_runmodes = {
|
||||
RunMode.BACKTEST: 'backtesting',
|
||||
}
|
||||
if method in no_unlimited_runmodes.keys():
|
||||
from freqtrade.data.btanalysis import get_latest_backtest_filename
|
||||
|
||||
if 'exportfilename' in config:
|
||||
if config['exportfilename'].is_dir():
|
||||
btfile = Path(get_latest_backtest_filename(config['exportfilename']))
|
||||
signals_file = f"{config['exportfilename']}/{btfile.stem}_signals.pkl"
|
||||
else:
|
||||
if config['exportfilename'].exists():
|
||||
btfile = Path(config['exportfilename'])
|
||||
signals_file = f"{btfile.parent}/{btfile.stem}_signals.pkl"
|
||||
else:
|
||||
raise OperationalException(f"{config['exportfilename']} does not exist.")
|
||||
else:
|
||||
raise OperationalException('exportfilename not in config.')
|
||||
|
||||
if (not Path(signals_file).exists()):
|
||||
raise OperationalException(
|
||||
(f"Cannot find latest backtest signals file: {signals_file}."
|
||||
"Run backtesting with `--export signals`.")
|
||||
)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def start_analysis_entries_exits(args: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Start analysis script
|
||||
:param args: Cli args from Arguments()
|
||||
:return: None
|
||||
"""
|
||||
from freqtrade.data.entryexitanalysis import process_entry_exit_reasons
|
||||
|
||||
# Initialize configuration
|
||||
config = setup_analyze_configuration(args, RunMode.BACKTEST)
|
||||
|
||||
logger.info('Starting freqtrade in analysis mode')
|
||||
|
||||
process_entry_exit_reasons(config['exportfilename'],
|
||||
config['exchange']['pair_whitelist'],
|
||||
config['analysis_groups'],
|
||||
config['enter_reason_list'],
|
||||
config['exit_reason_list'],
|
||||
config['indicator_list']
|
||||
)
|
||||
@@ -101,6 +101,9 @@ ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperop
|
||||
"print_json", "hyperoptexportfilename", "hyperopt_show_no_header",
|
||||
"disableparamexport", "backtest_breakdown"]
|
||||
|
||||
ARGS_ANALYZE_ENTRIES_EXITS = ["exportfilename", "analysis_groups", "enter_reason_list",
|
||||
"exit_reason_list", "indicator_list"]
|
||||
|
||||
NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes",
|
||||
"list-markets", "list-pairs", "list-strategies", "list-data",
|
||||
"hyperopt-list", "hyperopt-show", "backtest-filter",
|
||||
@@ -182,8 +185,9 @@ class Arguments:
|
||||
self.parser = argparse.ArgumentParser(description='Free, open source crypto trading bot')
|
||||
self._build_args(optionlist=['version'], parser=self.parser)
|
||||
|
||||
from freqtrade.commands import (start_backtesting, start_backtesting_show,
|
||||
start_convert_data, start_convert_db, start_convert_trades,
|
||||
from freqtrade.commands import (start_analysis_entries_exits, start_backtesting,
|
||||
start_backtesting_show, start_convert_data,
|
||||
start_convert_db, start_convert_trades,
|
||||
start_create_userdir, start_download_data, start_edge,
|
||||
start_hyperopt, start_hyperopt_list, start_hyperopt_show,
|
||||
start_install_ui, start_list_data, start_list_exchanges,
|
||||
@@ -283,6 +287,13 @@ class Arguments:
|
||||
backtesting_show_cmd.set_defaults(func=start_backtesting_show)
|
||||
self._build_args(optionlist=ARGS_BACKTEST_SHOW, parser=backtesting_show_cmd)
|
||||
|
||||
# Add backtesting analysis subcommand
|
||||
analysis_cmd = subparsers.add_parser('backtesting-analysis',
|
||||
help='Backtest Analysis module.',
|
||||
parents=[_common_parser])
|
||||
analysis_cmd.set_defaults(func=start_analysis_entries_exits)
|
||||
self._build_args(optionlist=ARGS_ANALYZE_ENTRIES_EXITS, parser=analysis_cmd)
|
||||
|
||||
# Add edge subcommand
|
||||
edge_cmd = subparsers.add_parser('edge', help='Edge module.',
|
||||
parents=[_common_parser, _strategy_parser])
|
||||
|
||||
@@ -614,4 +614,37 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
"that do not contain any parameters."),
|
||||
action="store_true",
|
||||
),
|
||||
"analysis_groups": Arg(
|
||||
"--analysis-groups",
|
||||
help=("grouping output - "
|
||||
"0: simple wins/losses by enter tag, "
|
||||
"1: by enter_tag, "
|
||||
"2: by enter_tag and exit_tag, "
|
||||
"3: by pair and enter_tag, "
|
||||
"4: by pair, enter_ and exit_tag (this can get quite large)"),
|
||||
nargs='+',
|
||||
default=['0', '1', '2'],
|
||||
choices=['0', '1', '2', '3', '4'],
|
||||
),
|
||||
"enter_reason_list": Arg(
|
||||
"--enter-reason-list",
|
||||
help=("Comma separated list of entry signals to analyse. Default: all. "
|
||||
"e.g. 'entry_tag_a,entry_tag_b'"),
|
||||
nargs='+',
|
||||
default=['all'],
|
||||
),
|
||||
"exit_reason_list": Arg(
|
||||
"--exit-reason-list",
|
||||
help=("Comma separated list of exit signals to analyse. Default: all. "
|
||||
"e.g. 'exit_tag_a,roi,stop_loss,trailing_stop_loss'"),
|
||||
nargs='+',
|
||||
default=['all'],
|
||||
),
|
||||
"indicator_list": Arg(
|
||||
"--indicator-list",
|
||||
help=("Comma separated list of indicators to analyse. "
|
||||
"e.g. 'close,rsi,bb_lowerband,profit_abs'"),
|
||||
nargs='+',
|
||||
default=[],
|
||||
),
|
||||
}
|
||||
|
||||
@@ -95,6 +95,8 @@ class Configuration:
|
||||
|
||||
self._process_data_options(config)
|
||||
|
||||
self._process_analyze_options(config)
|
||||
|
||||
# Check if the exchange set by the user is supported
|
||||
check_exchange(config, config.get('experimental', {}).get('block_bad_exchanges', True))
|
||||
|
||||
@@ -433,6 +435,19 @@ class Configuration:
|
||||
self._args_to_config(config, argname='candle_types',
|
||||
logstring='Detected --candle-types: {}')
|
||||
|
||||
def _process_analyze_options(self, config: Dict[str, Any]) -> None:
|
||||
self._args_to_config(config, argname='analysis_groups',
|
||||
logstring='Analysis reason groups: {}')
|
||||
|
||||
self._args_to_config(config, argname='enter_reason_list',
|
||||
logstring='Analysis enter tag list: {}')
|
||||
|
||||
self._args_to_config(config, argname='exit_reason_list',
|
||||
logstring='Analysis exit tag list: {}')
|
||||
|
||||
self._args_to_config(config, argname='indicator_list',
|
||||
logstring='Analysis indicator list: {}')
|
||||
|
||||
def _process_runmode(self, config: Dict[str, Any]) -> None:
|
||||
|
||||
self._args_to_config(config, argname='dry_run',
|
||||
|
||||
227
freqtrade/data/entryexitanalysis.py
Executable file
227
freqtrade/data/entryexitanalysis.py
Executable file
@@ -0,0 +1,227 @@
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
|
||||
import joblib
|
||||
import pandas as pd
|
||||
from tabulate import tabulate
|
||||
|
||||
from freqtrade.data.btanalysis import (get_latest_backtest_filename, load_backtest_data,
|
||||
load_backtest_stats)
|
||||
from freqtrade.exceptions import OperationalException
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _load_signal_candles(backtest_dir: Path):
|
||||
if backtest_dir.is_dir():
|
||||
scpf = Path(backtest_dir,
|
||||
Path(get_latest_backtest_filename(backtest_dir)).stem + "_signals.pkl"
|
||||
)
|
||||
else:
|
||||
scpf = Path(backtest_dir.parent / f"{backtest_dir.stem}_signals.pkl")
|
||||
|
||||
try:
|
||||
scp = open(scpf, "rb")
|
||||
signal_candles = joblib.load(scp)
|
||||
logger.info(f"Loaded signal candles: {str(scpf)}")
|
||||
except Exception as e:
|
||||
logger.error("Cannot load signal candles from pickled results: ", e)
|
||||
|
||||
return signal_candles
|
||||
|
||||
|
||||
def _process_candles_and_indicators(pairlist, strategy_name, trades, signal_candles):
|
||||
analysed_trades_dict = {}
|
||||
analysed_trades_dict[strategy_name] = {}
|
||||
|
||||
try:
|
||||
logger.info(f"Processing {strategy_name} : {len(pairlist)} pairs")
|
||||
|
||||
for pair in pairlist:
|
||||
if pair in signal_candles[strategy_name]:
|
||||
analysed_trades_dict[strategy_name][pair] = _analyze_candles_and_indicators(
|
||||
pair,
|
||||
trades,
|
||||
signal_candles[strategy_name][pair])
|
||||
except Exception as e:
|
||||
print(f"Cannot process entry/exit reasons for {strategy_name}: ", e)
|
||||
|
||||
return analysed_trades_dict
|
||||
|
||||
|
||||
def _analyze_candles_and_indicators(pair, trades, signal_candles):
|
||||
buyf = signal_candles
|
||||
|
||||
if len(buyf) > 0:
|
||||
buyf = buyf.set_index('date', drop=False)
|
||||
trades_red = trades.loc[trades['pair'] == pair].copy()
|
||||
|
||||
trades_inds = pd.DataFrame()
|
||||
|
||||
if trades_red.shape[0] > 0 and buyf.shape[0] > 0:
|
||||
for t, v in trades_red.open_date.items():
|
||||
allinds = buyf.loc[(buyf['date'] < v)]
|
||||
if allinds.shape[0] > 0:
|
||||
tmp_inds = allinds.iloc[[-1]]
|
||||
|
||||
trades_red.loc[t, 'signal_date'] = tmp_inds['date'].values[0]
|
||||
trades_red.loc[t, 'enter_reason'] = trades_red.loc[t, 'enter_tag']
|
||||
tmp_inds.index.rename('signal_date', inplace=True)
|
||||
trades_inds = pd.concat([trades_inds, tmp_inds])
|
||||
|
||||
if 'signal_date' in trades_red:
|
||||
trades_red['signal_date'] = pd.to_datetime(trades_red['signal_date'], utc=True)
|
||||
trades_red.set_index('signal_date', inplace=True)
|
||||
|
||||
try:
|
||||
trades_red = pd.merge(trades_red, trades_inds, on='signal_date', how='outer')
|
||||
except Exception as e:
|
||||
raise e
|
||||
return trades_red
|
||||
else:
|
||||
return pd.DataFrame()
|
||||
|
||||
|
||||
def _do_group_table_output(bigdf, glist):
|
||||
for g in glist:
|
||||
# 0: summary wins/losses grouped by enter tag
|
||||
if g == "0":
|
||||
group_mask = ['enter_reason']
|
||||
wins = bigdf.loc[bigdf['profit_abs'] >= 0] \
|
||||
.groupby(group_mask) \
|
||||
.agg({'profit_abs': ['sum']})
|
||||
|
||||
wins.columns = ['profit_abs_wins']
|
||||
loss = bigdf.loc[bigdf['profit_abs'] < 0] \
|
||||
.groupby(group_mask) \
|
||||
.agg({'profit_abs': ['sum']})
|
||||
loss.columns = ['profit_abs_loss']
|
||||
|
||||
new = bigdf.groupby(group_mask).agg({'profit_abs': [
|
||||
'count',
|
||||
lambda x: sum(x > 0),
|
||||
lambda x: sum(x <= 0)]})
|
||||
new = pd.concat([new, wins, loss], axis=1).fillna(0)
|
||||
|
||||
new['profit_tot'] = new['profit_abs_wins'] - abs(new['profit_abs_loss'])
|
||||
new['wl_ratio_pct'] = (new.iloc[:, 1] / new.iloc[:, 0] * 100).fillna(0)
|
||||
new['avg_win'] = (new['profit_abs_wins'] / new.iloc[:, 1]).fillna(0)
|
||||
new['avg_loss'] = (new['profit_abs_loss'] / new.iloc[:, 2]).fillna(0)
|
||||
|
||||
new.columns = ['total_num_buys', 'wins', 'losses', 'profit_abs_wins', 'profit_abs_loss',
|
||||
'profit_tot', 'wl_ratio_pct', 'avg_win', 'avg_loss']
|
||||
|
||||
sortcols = ['total_num_buys']
|
||||
|
||||
_print_table(new, sortcols, show_index=True)
|
||||
|
||||
else:
|
||||
agg_mask = {'profit_abs': ['count', 'sum', 'median', 'mean'],
|
||||
'profit_ratio': ['sum', 'median', 'mean']}
|
||||
agg_cols = ['num_buys', 'profit_abs_sum', 'profit_abs_median',
|
||||
'profit_abs_mean', 'median_profit_pct', 'mean_profit_pct',
|
||||
'total_profit_pct']
|
||||
sortcols = ['profit_abs_sum', 'enter_reason']
|
||||
|
||||
# 1: profit summaries grouped by enter_tag
|
||||
if g == "1":
|
||||
group_mask = ['enter_reason']
|
||||
|
||||
# 2: profit summaries grouped by enter_tag and exit_tag
|
||||
if g == "2":
|
||||
group_mask = ['enter_reason', 'exit_reason']
|
||||
|
||||
# 3: profit summaries grouped by pair and enter_tag
|
||||
if g == "3":
|
||||
group_mask = ['pair', 'enter_reason']
|
||||
|
||||
# 4: profit summaries grouped by pair, enter_ and exit_tag (this can get quite large)
|
||||
if g == "4":
|
||||
group_mask = ['pair', 'enter_reason', 'exit_reason']
|
||||
if group_mask:
|
||||
new = bigdf.groupby(group_mask).agg(agg_mask).reset_index()
|
||||
new.columns = group_mask + agg_cols
|
||||
new['median_profit_pct'] = new['median_profit_pct'] * 100
|
||||
new['mean_profit_pct'] = new['mean_profit_pct'] * 100
|
||||
new['total_profit_pct'] = new['total_profit_pct'] * 100
|
||||
|
||||
_print_table(new, sortcols)
|
||||
else:
|
||||
logger.warning("Invalid group mask specified.")
|
||||
|
||||
|
||||
def _print_results(analysed_trades, stratname, analysis_groups,
|
||||
enter_reason_list, exit_reason_list,
|
||||
indicator_list, columns=None):
|
||||
if columns is None:
|
||||
columns = ['pair', 'open_date', 'close_date', 'profit_abs', 'enter_reason', 'exit_reason']
|
||||
|
||||
bigdf = pd.DataFrame()
|
||||
for pair, trades in analysed_trades[stratname].items():
|
||||
bigdf = pd.concat([bigdf, trades], ignore_index=True)
|
||||
|
||||
if bigdf.shape[0] > 0 and ('enter_reason' in bigdf.columns):
|
||||
if analysis_groups:
|
||||
_do_group_table_output(bigdf, analysis_groups)
|
||||
|
||||
if enter_reason_list and "all" not in enter_reason_list:
|
||||
bigdf = bigdf.loc[(bigdf['enter_reason'].isin(enter_reason_list))]
|
||||
|
||||
if exit_reason_list and "all" not in exit_reason_list:
|
||||
bigdf = bigdf.loc[(bigdf['exit_reason'].isin(exit_reason_list))]
|
||||
|
||||
if "all" in indicator_list:
|
||||
print(bigdf)
|
||||
elif indicator_list is not None:
|
||||
available_inds = []
|
||||
for ind in indicator_list:
|
||||
if ind in bigdf:
|
||||
available_inds.append(ind)
|
||||
ilist = ["pair", "enter_reason", "exit_reason"] + available_inds
|
||||
_print_table(bigdf[ilist], sortcols=['exit_reason'], show_index=False)
|
||||
else:
|
||||
print("\\_ No trades to show")
|
||||
|
||||
|
||||
def _print_table(df, sortcols=None, show_index=False):
|
||||
if (sortcols is not None):
|
||||
data = df.sort_values(sortcols)
|
||||
else:
|
||||
data = df
|
||||
|
||||
print(
|
||||
tabulate(
|
||||
data,
|
||||
headers='keys',
|
||||
tablefmt='psql',
|
||||
showindex=show_index
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def process_entry_exit_reasons(backtest_dir: Path,
|
||||
pairlist: List[str],
|
||||
analysis_groups: Optional[List[str]] = ["0", "1", "2"],
|
||||
enter_reason_list: Optional[List[str]] = ["all"],
|
||||
exit_reason_list: Optional[List[str]] = ["all"],
|
||||
indicator_list: Optional[List[str]] = []):
|
||||
try:
|
||||
backtest_stats = load_backtest_stats(backtest_dir)
|
||||
for strategy_name, results in backtest_stats['strategy'].items():
|
||||
trades = load_backtest_data(backtest_dir, strategy_name)
|
||||
|
||||
if not trades.empty:
|
||||
signal_candles = _load_signal_candles(backtest_dir)
|
||||
analysed_trades_dict = _process_candles_and_indicators(pairlist, strategy_name,
|
||||
trades, signal_candles)
|
||||
_print_results(analysed_trades_dict,
|
||||
strategy_name,
|
||||
analysis_groups,
|
||||
enter_reason_list,
|
||||
exit_reason_list,
|
||||
indicator_list)
|
||||
|
||||
except ValueError as e:
|
||||
raise OperationalException(e) from e
|
||||
@@ -93,7 +93,7 @@ class Exchange:
|
||||
:return: None
|
||||
"""
|
||||
self._api: ccxt.Exchange
|
||||
self._api_async: ccxt_async.Exchange
|
||||
self._api_async: ccxt_async.Exchange = None
|
||||
self._markets: Dict = {}
|
||||
self._trading_fees: Dict[str, Any] = {}
|
||||
self._leverage_tiers: Dict[str, List[Dict]] = {}
|
||||
|
||||
@@ -1297,13 +1297,14 @@ class Backtesting:
|
||||
self.results['strategy_comparison'].extend(results['strategy_comparison'])
|
||||
else:
|
||||
self.results = results
|
||||
|
||||
dt_appendix = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
||||
if self.config.get('export', 'none') in ('trades', 'signals'):
|
||||
store_backtest_stats(self.config['exportfilename'], self.results)
|
||||
store_backtest_stats(self.config['exportfilename'], self.results, dt_appendix)
|
||||
|
||||
if (self.config.get('export', 'none') == 'signals' and
|
||||
self.dataprovider.runmode == RunMode.BACKTEST):
|
||||
store_backtest_signal_candles(self.config['exportfilename'], self.processed_dfs)
|
||||
store_backtest_signal_candles(
|
||||
self.config['exportfilename'], self.processed_dfs, dt_appendix)
|
||||
|
||||
# Results may be mixed up now. Sort them so they follow --strategy-list order.
|
||||
if 'strategy_list' in self.config and len(self.results) > 0:
|
||||
|
||||
@@ -17,21 +17,21 @@ from freqtrade.optimize.backtest_caching import get_backtest_metadata_filename
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def store_backtest_stats(recordfilename: Path, stats: Dict[str, DataFrame]) -> None:
|
||||
def store_backtest_stats(
|
||||
recordfilename: Path, stats: Dict[str, DataFrame], dtappendix: str) -> None:
|
||||
"""
|
||||
Stores backtest results
|
||||
:param recordfilename: Path object, which can either be a filename or a directory.
|
||||
Filenames will be appended with a timestamp right before the suffix
|
||||
while for directories, <directory>/backtest-result-<datetime>.json will be used as filename
|
||||
:param stats: Dataframe containing the backtesting statistics
|
||||
:param dtappendix: Datetime to use for the filename
|
||||
"""
|
||||
if recordfilename.is_dir():
|
||||
filename = (recordfilename /
|
||||
f'backtest-result-{datetime.now().strftime("%Y-%m-%d_%H-%M-%S")}.json')
|
||||
filename = (recordfilename / f'backtest-result-{dtappendix}.json')
|
||||
else:
|
||||
filename = Path.joinpath(
|
||||
recordfilename.parent,
|
||||
f'{recordfilename.stem}-{datetime.now().strftime("%Y-%m-%d_%H-%M-%S")}'
|
||||
recordfilename.parent, f'{recordfilename.stem}-{dtappendix}'
|
||||
).with_suffix(recordfilename.suffix)
|
||||
|
||||
# Store metadata separately.
|
||||
@@ -44,7 +44,8 @@ def store_backtest_stats(recordfilename: Path, stats: Dict[str, DataFrame]) -> N
|
||||
file_dump_json(latest_filename, {'latest_backtest': str(filename.name)})
|
||||
|
||||
|
||||
def store_backtest_signal_candles(recordfilename: Path, candles: Dict[str, Dict]) -> Path:
|
||||
def store_backtest_signal_candles(
|
||||
recordfilename: Path, candles: Dict[str, Dict], dtappendix: str) -> Path:
|
||||
"""
|
||||
Stores backtest trade signal candles
|
||||
:param recordfilename: Path object, which can either be a filename or a directory.
|
||||
@@ -52,14 +53,13 @@ def store_backtest_signal_candles(recordfilename: Path, candles: Dict[str, Dict]
|
||||
while for directories, <directory>/backtest-result-<datetime>_signals.pkl will be used
|
||||
as filename
|
||||
:param stats: Dict containing the backtesting signal candles
|
||||
:param dtappendix: Datetime to use for the filename
|
||||
"""
|
||||
if recordfilename.is_dir():
|
||||
filename = (recordfilename /
|
||||
f'backtest-result-{datetime.now().strftime("%Y-%m-%d_%H-%M-%S")}_signals.pkl')
|
||||
filename = (recordfilename / f'backtest-result-{dtappendix}_signals.pkl')
|
||||
else:
|
||||
filename = Path.joinpath(
|
||||
recordfilename.parent,
|
||||
f'{recordfilename.stem}-{datetime.now().strftime("%Y-%m-%d_%H-%M-%S")}_signals.pkl'
|
||||
recordfilename.parent, f'{recordfilename.stem}-{dtappendix}_signals.pkl'
|
||||
)
|
||||
|
||||
file_dump_joblib(filename, candles)
|
||||
|
||||
@@ -666,8 +666,8 @@ class LocalTrade():
|
||||
"""
|
||||
self.close_rate = rate
|
||||
self.close_date = self.close_date or datetime.utcnow()
|
||||
self.close_profit = self.calc_profit_ratio()
|
||||
self.close_profit_abs = self.calc_profit() + self.realized_profit
|
||||
self.close_profit = self.calc_profit_ratio(rate)
|
||||
self.close_profit_abs = self.calc_profit(rate) + self.realized_profit
|
||||
self.is_open = False
|
||||
self.exit_order_status = 'closed'
|
||||
self.open_order_id = None
|
||||
@@ -716,12 +716,12 @@ class LocalTrade():
|
||||
"""
|
||||
return len([o for o in self.orders if o.ft_order_side == self.exit_side])
|
||||
|
||||
def _calc_open_trade_value(self) -> float:
|
||||
def _calc_open_trade_value(self, amount: float, open_rate: float) -> float:
|
||||
"""
|
||||
Calculate the open_rate including open_fee.
|
||||
:return: Price in of the open trade incl. Fees
|
||||
"""
|
||||
open_trade = Decimal(self.amount) * Decimal(self.open_rate)
|
||||
open_trade = Decimal(amount) * Decimal(open_rate)
|
||||
fees = open_trade * Decimal(self.fee_open)
|
||||
if self.is_short:
|
||||
return float(open_trade - fees)
|
||||
@@ -733,12 +733,11 @@ class LocalTrade():
|
||||
Recalculate open_trade_value.
|
||||
Must be called whenever open_rate, fee_open is changed.
|
||||
"""
|
||||
self.open_trade_value = self._calc_open_trade_value()
|
||||
self.open_trade_value = self._calc_open_trade_value(self.amount, self.open_rate)
|
||||
|
||||
def calculate_interest(self, interest_rate: Optional[float] = None) -> Decimal:
|
||||
def calculate_interest(self) -> Decimal:
|
||||
"""
|
||||
:param interest_rate: interest_charge for borrowing this coin(optional).
|
||||
If interest_rate is not set self.interest_rate will be used
|
||||
Calculate interest for this trade. Only applicable for Margin trading.
|
||||
"""
|
||||
zero = Decimal(0.0)
|
||||
# If nothing was borrowed
|
||||
@@ -751,90 +750,77 @@ class LocalTrade():
|
||||
total_seconds = Decimal((now - open_date).total_seconds())
|
||||
hours = total_seconds / sec_per_hour or zero
|
||||
|
||||
rate = Decimal(interest_rate or self.interest_rate)
|
||||
rate = Decimal(self.interest_rate)
|
||||
borrowed = Decimal(self.borrowed)
|
||||
|
||||
return interest(exchange_name=self.exchange, borrowed=borrowed, rate=rate, hours=hours)
|
||||
|
||||
def _calc_base_close(self, amount: Decimal, rate: Optional[float] = None,
|
||||
fee: Optional[float] = None) -> Decimal:
|
||||
def _calc_base_close(self, amount: Decimal, rate: float, fee: float) -> Decimal:
|
||||
|
||||
close_trade = Decimal(amount) * Decimal(rate or self.close_rate) # type: ignore
|
||||
fees = close_trade * Decimal(fee or self.fee_close)
|
||||
close_trade = amount * Decimal(rate)
|
||||
fees = close_trade * Decimal(fee)
|
||||
|
||||
if self.is_short:
|
||||
return close_trade + fees
|
||||
else:
|
||||
return close_trade - fees
|
||||
|
||||
def calc_close_trade_value(self, rate: Optional[float] = None,
|
||||
fee: Optional[float] = None,
|
||||
interest_rate: Optional[float] = None) -> float:
|
||||
def calc_close_trade_value(self, rate: float, amount: float = None) -> float:
|
||||
"""
|
||||
Calculate the close_rate including fee
|
||||
:param fee: fee to use on the close rate (optional).
|
||||
If rate is not set self.fee will be used
|
||||
:param rate: rate to compare with (optional).
|
||||
If rate is not set self.close_rate will be used
|
||||
:param interest_rate: interest_charge for borrowing this coin (optional).
|
||||
If interest_rate is not set self.interest_rate will be used
|
||||
:return: Price in BTC of the open trade
|
||||
Calculate the Trade's close value including fees
|
||||
:param rate: rate to compare with.
|
||||
:return: value in stake currency of the open trade
|
||||
"""
|
||||
if rate is None and not self.close_rate:
|
||||
return 0.0
|
||||
|
||||
amount = Decimal(self.amount)
|
||||
amount = Decimal(amount or self.amount)
|
||||
trading_mode = self.trading_mode or TradingMode.SPOT
|
||||
|
||||
if trading_mode == TradingMode.SPOT:
|
||||
return float(self._calc_base_close(amount, rate, fee))
|
||||
return float(self._calc_base_close(amount, rate, self.fee_close))
|
||||
|
||||
elif (trading_mode == TradingMode.MARGIN):
|
||||
|
||||
total_interest = self.calculate_interest(interest_rate)
|
||||
total_interest = self.calculate_interest()
|
||||
|
||||
if self.is_short:
|
||||
amount = amount + total_interest
|
||||
return float(self._calc_base_close(amount, rate, fee))
|
||||
return float(self._calc_base_close(amount, rate, self.fee_close))
|
||||
else:
|
||||
# Currency already owned for longs, no need to purchase
|
||||
return float(self._calc_base_close(amount, rate, fee) - total_interest)
|
||||
return float(self._calc_base_close(amount, rate, self.fee_close) - total_interest)
|
||||
|
||||
elif (trading_mode == TradingMode.FUTURES):
|
||||
funding_fees = self.funding_fees or 0.0
|
||||
# Positive funding_fees -> Trade has gained from fees.
|
||||
# Negative funding_fees -> Trade had to pay the fees.
|
||||
if self.is_short:
|
||||
return float(self._calc_base_close(amount, rate, fee)) - funding_fees
|
||||
return float(self._calc_base_close(amount, rate, self.fee_close)) - funding_fees
|
||||
else:
|
||||
return float(self._calc_base_close(amount, rate, fee)) + funding_fees
|
||||
return float(self._calc_base_close(amount, rate, self.fee_close)) + funding_fees
|
||||
else:
|
||||
raise OperationalException(
|
||||
f"{self.trading_mode.value} trading is not yet available using freqtrade")
|
||||
|
||||
def calc_profit(self, rate: Optional[float] = None,
|
||||
fee: Optional[float] = None,
|
||||
interest_rate: Optional[float] = None) -> float:
|
||||
def calc_profit(self, rate: float, amount: float = None, open_rate: float = None) -> float:
|
||||
"""
|
||||
Calculate the absolute profit in stake currency between Close and Open trade
|
||||
:param fee: fee to use on the close rate (optional).
|
||||
If fee is not set self.fee will be used
|
||||
:param rate: close rate to compare with (optional).
|
||||
If rate is not set self.close_rate will be used
|
||||
:param interest_rate: interest_charge for borrowing this coin (optional).
|
||||
If interest_rate is not set self.interest_rate will be used
|
||||
:return: profit in stake currency as float
|
||||
:param rate: close rate to compare with.
|
||||
:param amount: Amount to use for the calculation. Falls back to trade.amount if not set.
|
||||
:param open_rate: open_rate to use. Defaults to self.open_rate if not provided.
|
||||
:return: profit in stake currency as float
|
||||
"""
|
||||
close_trade_value = self.calc_close_trade_value(
|
||||
rate=(rate or self.close_rate),
|
||||
fee=(fee or self.fee_close),
|
||||
interest_rate=(interest_rate or self.interest_rate)
|
||||
)
|
||||
close_trade_value = self.calc_close_trade_value(rate, amount)
|
||||
if amount is None or open_rate is None:
|
||||
open_trade_value = self.open_trade_value
|
||||
else:
|
||||
open_trade_value = self._calc_open_trade_value(amount, open_rate)
|
||||
|
||||
if self.is_short:
|
||||
profit = self.open_trade_value - close_trade_value
|
||||
profit = open_trade_value - close_trade_value
|
||||
else:
|
||||
profit = close_trade_value - self.open_trade_value
|
||||
profit = close_trade_value - open_trade_value
|
||||
return float(f"{profit:.8f}")
|
||||
|
||||
def calc_profit2(self, open_rate: float, close_rate: float,
|
||||
@@ -845,35 +831,33 @@ class LocalTrade():
|
||||
* (Decimal(1 - self.fee_close) * Decimal(close_rate)
|
||||
- Decimal(1 + self.fee_open) * Decimal(open_rate)))
|
||||
|
||||
def calc_profit_ratio(self, rate: Optional[float] = None,
|
||||
fee: Optional[float] = None,
|
||||
interest_rate: Optional[float] = None) -> float:
|
||||
def calc_profit_ratio(
|
||||
self, rate: float, amount: float = None, open_rate: float = None) -> float:
|
||||
"""
|
||||
Calculates the profit as ratio (including fee).
|
||||
:param rate: rate to compare with (optional).
|
||||
If rate is not set self.close_rate will be used
|
||||
:param fee: fee to use on the close rate (optional).
|
||||
:param interest_rate: interest_charge for borrowing this coin (optional).
|
||||
If interest_rate is not set self.interest_rate will be used
|
||||
:param rate: rate to compare with.
|
||||
:param amount: Amount to use for the calculation. Falls back to trade.amount if not set.
|
||||
:param open_rate: open_rate to use. Defaults to self.open_rate if not provided.
|
||||
:return: profit ratio as float
|
||||
"""
|
||||
close_trade_value = self.calc_close_trade_value(
|
||||
rate=(rate or self.close_rate),
|
||||
fee=(fee or self.fee_close),
|
||||
interest_rate=(interest_rate or self.interest_rate)
|
||||
)
|
||||
close_trade_value = self.calc_close_trade_value(rate, amount)
|
||||
|
||||
if amount is None or open_rate is None:
|
||||
open_trade_value = self.open_trade_value
|
||||
else:
|
||||
open_trade_value = self._calc_open_trade_value(amount, open_rate)
|
||||
|
||||
short_close_zero = (self.is_short and close_trade_value == 0.0)
|
||||
long_close_zero = (not self.is_short and self.open_trade_value == 0.0)
|
||||
long_close_zero = (not self.is_short and open_trade_value == 0.0)
|
||||
leverage = self.leverage or 1.0
|
||||
|
||||
if (short_close_zero or long_close_zero):
|
||||
return 0.0
|
||||
else:
|
||||
if self.is_short:
|
||||
profit_ratio = (1 - (close_trade_value / self.open_trade_value)) * leverage
|
||||
profit_ratio = (1 - (close_trade_value / open_trade_value)) * leverage
|
||||
else:
|
||||
profit_ratio = ((close_trade_value / self.open_trade_value) - 1) * leverage
|
||||
profit_ratio = ((close_trade_value / open_trade_value) - 1) * leverage
|
||||
|
||||
return float(f"{profit_ratio:.8f}")
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends
|
||||
@@ -102,7 +103,10 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac
|
||||
min_date=min_date, max_date=max_date)
|
||||
|
||||
if btconfig.get('export', 'none') == 'trades':
|
||||
store_backtest_stats(btconfig['exportfilename'], ApiServer._bt.results)
|
||||
store_backtest_stats(
|
||||
btconfig['exportfilename'], ApiServer._bt.results,
|
||||
datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
||||
)
|
||||
|
||||
logger.info("Backtest finished.")
|
||||
|
||||
|
||||
@@ -512,7 +512,7 @@ class RPC:
|
||||
|
||||
def _rpc_balance(self, stake_currency: str, fiat_display_currency: str) -> Dict:
|
||||
""" Returns current account balance per crypto """
|
||||
currencies = []
|
||||
currencies: List[Dict] = []
|
||||
total = 0.0
|
||||
try:
|
||||
tickers = self._freqtrade.exchange.get_tickers(cached=True)
|
||||
@@ -547,13 +547,12 @@ class RPC:
|
||||
except (ExchangeError):
|
||||
logger.warning(f" Could not get rate for pair {coin}.")
|
||||
continue
|
||||
total = total + (est_stake or 0)
|
||||
total = total + est_stake
|
||||
currencies.append({
|
||||
'currency': coin,
|
||||
# TODO: The below can be simplified if we don't assign None to values.
|
||||
'free': balance.free if balance.free is not None else 0,
|
||||
'balance': balance.total if balance.total is not None else 0,
|
||||
'used': balance.used if balance.used is not None else 0,
|
||||
'free': balance.free,
|
||||
'balance': balance.total,
|
||||
'used': balance.used,
|
||||
'est_stake': est_stake or 0,
|
||||
'stake': stake_currency,
|
||||
'side': 'long',
|
||||
@@ -583,7 +582,6 @@ class RPC:
|
||||
total, stake_currency, fiat_display_currency) if self._fiat_converter else 0
|
||||
|
||||
trade_count = len(Trade.get_trades_proxy())
|
||||
starting_capital_ratio = 0.0
|
||||
starting_capital_ratio = (total / starting_capital) - 1 if starting_capital else 0.0
|
||||
starting_cap_fiat_ratio = (value / starting_cap_fiat) - 1 if starting_cap_fiat else 0.0
|
||||
|
||||
@@ -871,7 +869,7 @@ class RPC:
|
||||
else:
|
||||
errors[pair] = {
|
||||
'error_msg': f"Pair {pair} is not in the current blacklist."
|
||||
}
|
||||
}
|
||||
resp = self._rpc_blacklist()
|
||||
resp['errors'] = errors
|
||||
return resp
|
||||
|
||||
Reference in New Issue
Block a user