Merge branch 'develop' into feat_readjust_entry

This commit is contained in:
eSeR1805 2022-04-16 15:20:50 +03:00
commit d65b64a46f
No known key found for this signature in database
GPG Key ID: BA53686259B46936
35 changed files with 247 additions and 110 deletions

View File

@ -64,7 +64,10 @@ Binance supports [time_in_force](configuration.md#understand-order_time_in_force
For Binance, please add `"BNB/<STAKE>"` to your blacklist to avoid issues. For Binance, please add `"BNB/<STAKE>"` to your blacklist to avoid issues.
Accounts having BNB accounts use this to pay for fees - if your first trade happens to be on `BNB`, further trades will consume this position and make the initial BNB trade unsellable as the expected amount is not there anymore. Accounts having BNB accounts use this to pay for fees - if your first trade happens to be on `BNB`, further trades will consume this position and make the initial BNB trade unsellable as the expected amount is not there anymore.
### Binance Futures' order pricing ### Binance Futures
Binance has specific (unfortunately complex) [Futures Trading Quantitative Rules](https://www.binance.com/en/support/faq/4f462ebe6ff445d4a170be7d9e897272) which need to be followed, and which prohibit a too low stake-amount (among others) for too many orders.
Violating these rules will result in a trading restriction.
When trading on Binance Futures market, orderbook must be used because there is no price ticker data for futures. When trading on Binance Futures market, orderbook must be used because there is no price ticker data for futures.

View File

@ -419,7 +419,7 @@ The function must return either `True` (cancel order) or `False` (keep order ali
``` python ``` python
from datetime import datetime, timedelta from datetime import datetime, timedelta
from freqtrade.persistence import Trade from freqtrade.persistence import Trade, Order
class AwesomeStrategy(IStrategy): class AwesomeStrategy(IStrategy):
@ -431,7 +431,7 @@ class AwesomeStrategy(IStrategy):
'exit': 60 * 25 'exit': 60 * 25
} }
def check_entry_timeout(self, pair: str, trade: 'Trade', order: dict, def check_entry_timeout(self, pair: str, trade: 'Trade', order: 'Order',
current_time: datetime, **kwargs) -> bool: current_time: datetime, **kwargs) -> bool:
if trade.open_rate > 100 and trade.open_date_utc < current_time - timedelta(minutes=5): if trade.open_rate > 100 and trade.open_date_utc < current_time - timedelta(minutes=5):
return True return True
@ -442,7 +442,7 @@ class AwesomeStrategy(IStrategy):
return False return False
def check_exit_timeout(self, pair: str, trade: Trade, order: dict, def check_exit_timeout(self, pair: str, trade: Trade, order: 'Order',
current_time: datetime, **kwargs) -> bool: current_time: datetime, **kwargs) -> bool:
if trade.open_rate > 100 and trade.open_date_utc < current_time - timedelta(minutes=5): if trade.open_rate > 100 and trade.open_date_utc < current_time - timedelta(minutes=5):
return True return True
@ -460,7 +460,7 @@ class AwesomeStrategy(IStrategy):
``` python ``` python
from datetime import datetime from datetime import datetime
from freqtrade.persistence import Trade from freqtrade.persistence import Trade, Order
class AwesomeStrategy(IStrategy): class AwesomeStrategy(IStrategy):
@ -472,22 +472,22 @@ class AwesomeStrategy(IStrategy):
'exit': 60 * 25 'exit': 60 * 25
} }
def check_entry_timeout(self, pair: str, trade: Trade, order: dict, def check_entry_timeout(self, pair: str, trade: 'Trade', order: 'Order',
current_time: datetime, **kwargs) -> bool: current_time: datetime, **kwargs) -> bool:
ob = self.dp.orderbook(pair, 1) ob = self.dp.orderbook(pair, 1)
current_price = ob['bids'][0][0] current_price = ob['bids'][0][0]
# Cancel buy order if price is more than 2% above the order. # Cancel buy order if price is more than 2% above the order.
if current_price > order['price'] * 1.02: if current_price > order.price * 1.02:
return True return True
return False return False
def check_exit_timeout(self, pair: str, trade: Trade, order: dict, def check_exit_timeout(self, pair: str, trade: 'Trade', order: 'Order',
current_time: datetime, **kwargs) -> bool: current_time: datetime, **kwargs) -> bool:
ob = self.dp.orderbook(pair, 1) ob = self.dp.orderbook(pair, 1)
current_price = ob['asks'][0][0] current_price = ob['asks'][0][0]
# Cancel sell order if price is more than 2% below the order. # Cancel sell order if price is more than 2% below the order.
if current_price < order['price'] * 0.98: if current_price < order.price * 0.98:
return True return True
return False return False
``` ```

View File

@ -183,11 +183,11 @@ class AwesomeStrategy(IStrategy):
``` python hl_lines="2 6" ``` python hl_lines="2 6"
class AwesomeStrategy(IStrategy): class AwesomeStrategy(IStrategy):
def check_entry_timeout(self, pair: str, trade: 'Trade', order: dict, def check_entry_timeout(self, pair: str, trade: 'Trade', order: 'Order',
current_time: datetime, **kwargs) -> bool: current_time: datetime, **kwargs) -> bool:
return False return False
def check_exit_timeout(self, pair: str, trade: 'Trade', order: dict, def check_exit_timeout(self, pair: str, trade: 'Trade', order: 'Order',
current_time: datetime, **kwargs) -> bool: current_time: datetime, **kwargs) -> bool:
return False return False
``` ```

View File

@ -2,6 +2,10 @@
To update your freqtrade installation, please use one of the below methods, corresponding to your installation method. To update your freqtrade installation, please use one of the below methods, corresponding to your installation method.
!!! Note "Tracking changes"
Breaking changes / changed behavior will be documented in the changelog that is posted alongside every release.
For the develop branch, please follow PR's to avoid being surprised by changes.
## docker-compose ## docker-compose
!!! Note "Legacy installations using the `master` image" !!! Note "Legacy installations using the `master` image"

View File

@ -16,4 +16,4 @@ class PeriodicCache(TTLCache):
return ts - offset return ts - offset
# Init with smlight offset # Init with smlight offset
super().__init__(maxsize=maxsize, ttl=ttl-1e-5, timer=local_timer, getsizeof=getsizeof) super().__init__(maxsize=maxsize, ttl=ttl - 1e-5, timer=local_timer, getsizeof=getsizeof)

View File

@ -31,7 +31,7 @@ def log_config_error_range(path: str, errmsg: str) -> str:
offset = int(offsetlist[0]) offset = int(offsetlist[0])
text = Path(path).read_text() text = Path(path).read_text()
# Fetch an offset of 80 characters around the error line # Fetch an offset of 80 characters around the error line
subtext = text[offset-min(80, offset):offset+80] subtext = text[offset - min(80, offset):offset + 80]
segments = subtext.split('\n') segments = subtext.split('\n')
if len(segments) > 3: if len(segments) > 3:
# Remove first and last lines, to avoid odd truncations # Remove first and last lines, to avoid odd truncations

View File

@ -149,7 +149,14 @@ def load_backtest_stats(filename: Union[Path, str]) -> Dict[str, Any]:
return data return data
def _load_and_merge_backtest_result(strategy_name: str, filename: Path, results: Dict[str, Any]): def load_and_merge_backtest_result(strategy_name: str, filename: Path, results: Dict[str, Any]):
"""
Load one strategy from multi-strategy result
and merge it with results
:param strategy_name: Name of the strategy contained in the result
:param filename: Backtest-result-filename to load
:param results: dict to merge the result to.
"""
bt_data = load_backtest_stats(filename) bt_data = load_backtest_stats(filename)
for k in ('metadata', 'strategy'): for k in ('metadata', 'strategy'):
results[k][strategy_name] = bt_data[k][strategy_name] results[k][strategy_name] = bt_data[k][strategy_name]
@ -160,6 +167,30 @@ def _load_and_merge_backtest_result(strategy_name: str, filename: Path, results:
break break
def _get_backtest_files(dirname: Path) -> List[Path]:
return list(reversed(sorted(dirname.glob('backtest-result-*-[0-9][0-9].json'))))
def get_backtest_resultlist(dirname: Path):
"""
Get list of backtest results read from metadata files
"""
results = []
for filename in _get_backtest_files(dirname):
metadata = load_backtest_metadata(filename)
if not metadata:
continue
for s, v in metadata.items():
results.append({
'filename': filename.name,
'strategy': s,
'run_id': v['run_id'],
'backtest_start_time': v['backtest_start_time'],
})
return results
def find_existing_backtest_stats(dirname: Union[Path, str], run_ids: Dict[str, str], def find_existing_backtest_stats(dirname: Union[Path, str], run_ids: Dict[str, str],
min_backtest_date: datetime = None) -> Dict[str, Any]: min_backtest_date: datetime = None) -> Dict[str, Any]:
""" """
@ -179,7 +210,7 @@ def find_existing_backtest_stats(dirname: Union[Path, str], run_ids: Dict[str, s
} }
# Weird glob expression here avoids including .meta.json files. # Weird glob expression here avoids including .meta.json files.
for filename in reversed(sorted(dirname.glob('backtest-result-*-[0-9][0-9].json'))): for filename in _get_backtest_files(dirname):
metadata = load_backtest_metadata(filename) metadata = load_backtest_metadata(filename)
if not metadata: if not metadata:
# Files are sorted from newest to oldest. When file without metadata is encountered it # Files are sorted from newest to oldest. When file without metadata is encountered it
@ -202,7 +233,7 @@ def find_existing_backtest_stats(dirname: Union[Path, str], run_ids: Dict[str, s
if strategy_metadata['run_id'] == run_id: if strategy_metadata['run_id'] == run_id:
del run_ids[strategy_name] del run_ids[strategy_name]
_load_and_merge_backtest_result(strategy_name, filename, results) load_and_merge_backtest_result(strategy_name, filename, results)
if len(run_ids) == 0: if len(run_ids) == 0:
break break

View File

@ -651,7 +651,7 @@ class Exchange:
Re-implementation of ccxt internal methods - ensuring we can test the result is correct Re-implementation of ccxt internal methods - ensuring we can test the result is correct
based on our definitions. based on our definitions.
""" """
if self.markets[pair]['precision']['amount']: if self.markets[pair]['precision']['amount'] is not None:
amount = float(decimal_to_precision(amount, rounding_mode=TRUNCATE, amount = float(decimal_to_precision(amount, rounding_mode=TRUNCATE,
precision=self.markets[pair]['precision']['amount'], precision=self.markets[pair]['precision']['amount'],
counting_mode=self.precisionMode, counting_mode=self.precisionMode,
@ -2177,7 +2177,7 @@ class Exchange:
lev = tier['lev'] lev = tier['lev']
if tier_index < len(pair_tiers) - 1: if tier_index < len(pair_tiers) - 1:
next_tier = pair_tiers[tier_index+1] next_tier = pair_tiers[tier_index + 1]
next_floor = next_tier['min'] / next_tier['lev'] next_floor = next_tier['min'] / next_tier['lev']
if next_floor > stake_amount: # Next tier min too high for stake amount if next_floor > stake_amount: # Next tier min too high for stake amount
return min((tier['max'] / stake_amount), lev) return min((tier['max'] / stake_amount), lev)

View File

@ -31,13 +31,13 @@ def interest(
""" """
exchange_name = exchange_name.lower() exchange_name = exchange_name.lower()
if exchange_name == "binance": if exchange_name == "binance":
return borrowed * rate * ceil(hours)/twenty_four return borrowed * rate * ceil(hours) / twenty_four
elif exchange_name == "kraken": elif exchange_name == "kraken":
# Rounded based on https://kraken-fees-calculator.github.io/ # Rounded based on https://kraken-fees-calculator.github.io/
return borrowed * rate * (one+ceil(hours/four)) return borrowed * rate * (one + ceil(hours / four))
elif exchange_name == "ftx": elif exchange_name == "ftx":
# As Explained under #Interest rates section in # As Explained under #Interest rates section in
# https://help.ftx.com/hc/en-us/articles/360053007671-Spot-Margin-Trading-Explainer # https://help.ftx.com/hc/en-us/articles/360053007671-Spot-Margin-Trading-Explainer
return borrowed * rate * ceil(hours)/twenty_four return borrowed * rate * ceil(hours) / twenty_four
else: else:
raise OperationalException(f"Leverage not available on {exchange_name} with freqtrade") raise OperationalException(f"Leverage not available on {exchange_name} with freqtrade")

View File

@ -126,7 +126,7 @@ def format_ms_time(date: int) -> str:
convert MS date to readable format. convert MS date to readable format.
: epoch-string in ms : epoch-string in ms
""" """
return datetime.fromtimestamp(date/1000.0).strftime('%Y-%m-%dT%H:%M:%S') return datetime.fromtimestamp(date / 1000.0).strftime('%Y-%m-%dT%H:%M:%S')
def deep_merge_dicts(source, destination, allow_null_overrides: bool = True): def deep_merge_dicts(source, destination, allow_null_overrides: bool = True):

View File

@ -390,8 +390,8 @@ class HyperoptTools():
lambda x: '{} {}'.format( lambda x: '{} {}'.format(
round_coin_value(x['Total profit'], stake_currency, keep_trailing_zeros=True), round_coin_value(x['Total profit'], stake_currency, keep_trailing_zeros=True),
f"({x['Profit']:,.2%})".rjust(10, ' ') f"({x['Profit']:,.2%})".rjust(10, ' ')
).rjust(25+len(stake_currency)) ).rjust(25 + len(stake_currency))
if x['Total profit'] != 0.0 else '--'.rjust(25+len(stake_currency)), if x['Total profit'] != 0.0 else '--'.rjust(25 + len(stake_currency)),
axis=1 axis=1
) )
trials = trials.drop(columns=['Total profit']) trials = trials.drop(columns=['Total profit'])
@ -399,11 +399,11 @@ class HyperoptTools():
if print_colorized: if print_colorized:
for i in range(len(trials)): for i in range(len(trials)):
if trials.loc[i]['is_profit']: if trials.loc[i]['is_profit']:
for j in range(len(trials.loc[i])-3): for j in range(len(trials.loc[i]) - 3):
trials.iat[i, j] = "{}{}{}".format(Fore.GREEN, trials.iat[i, j] = "{}{}{}".format(Fore.GREEN,
str(trials.loc[i][j]), Fore.RESET) str(trials.loc[i][j]), Fore.RESET)
if trials.loc[i]['is_best'] and highlight_best: if trials.loc[i]['is_best'] and highlight_best:
for j in range(len(trials.loc[i])-3): for j in range(len(trials.loc[i]) - 3):
trials.iat[i, j] = "{}{}{}".format(Style.BRIGHT, trials.iat[i, j] = "{}{}{}".format(Style.BRIGHT,
str(trials.loc[i][j]), Style.RESET_ALL) str(trials.loc[i][j]), Style.RESET_ALL)
@ -459,7 +459,7 @@ class HyperoptTools():
'loss', 'is_initial_point', 'is_best'] 'loss', 'is_initial_point', 'is_best']
perc_multi = 100 perc_multi = 100
param_metrics = [("params_dict."+param) for param in results[0]['params_dict'].keys()] param_metrics = [("params_dict." + param) for param in results[0]['params_dict'].keys()]
trials = trials[base_metrics + param_metrics] trials = trials[base_metrics + param_metrics]
base_columns = ['Best', 'Epoch', 'Trades', 'Avg profit', 'Median profit', 'Total profit', base_columns = ['Best', 'Epoch', 'Trades', 'Avg profit', 'Median profit', 'Total profit',

View File

@ -360,7 +360,7 @@ class LocalTrade():
if self.has_no_leverage: if self.has_no_leverage:
return 0.0 return 0.0
elif not self.is_short: elif not self.is_short:
return (self.amount * self.open_rate) * ((self.leverage-1)/self.leverage) return (self.amount * self.open_rate) * ((self.leverage - 1) / self.leverage)
else: else:
return self.amount return self.amount
@ -747,7 +747,7 @@ class LocalTrade():
now = (self.close_date or datetime.now(timezone.utc)).replace(tzinfo=None) now = (self.close_date or datetime.now(timezone.utc)).replace(tzinfo=None)
sec_per_hour = Decimal(3600) sec_per_hour = Decimal(3600)
total_seconds = Decimal((now - open_date).total_seconds()) total_seconds = Decimal((now - open_date).total_seconds())
hours = total_seconds/sec_per_hour or zero hours = total_seconds / sec_per_hour or zero
rate = Decimal(interest_rate or self.interest_rate) rate = Decimal(interest_rate or self.interest_rate)
borrowed = Decimal(self.borrowed) borrowed = Decimal(self.borrowed)
@ -861,9 +861,9 @@ class LocalTrade():
return 0.0 return 0.0
else: else:
if self.is_short: if self.is_short:
profit_ratio = (1 - (close_trade_value/self.open_trade_value)) * leverage profit_ratio = (1 - (close_trade_value / self.open_trade_value)) * leverage
else: else:
profit_ratio = ((close_trade_value/self.open_trade_value) - 1) * leverage profit_ratio = ((close_trade_value / self.open_trade_value) - 1) * leverage
return float(f"{profit_ratio:.8f}") return float(f"{profit_ratio:.8f}")

View File

@ -107,7 +107,7 @@ class VolatilityFilter(IPairList):
returns = (np.log(daily_candles.close / daily_candles.close.shift(-1))) returns = (np.log(daily_candles.close / daily_candles.close.shift(-1)))
returns.fillna(0, inplace=True) returns.fillna(0, inplace=True)
volatility_series = returns.rolling(window=self._days).std()*np.sqrt(self._days) volatility_series = returns.rolling(window=self._days).std() * np.sqrt(self._days)
volatility_avg = volatility_series.mean() volatility_avg = volatility_series.mean()
if self._min_volatility <= volatility_avg <= self._max_volatility: if self._min_volatility <= volatility_avg <= self._max_volatility:

View File

@ -1,13 +1,16 @@
import asyncio import asyncio
import logging import logging
from copy import deepcopy from copy import deepcopy
from typing import Any, Dict, List
from fastapi import APIRouter, BackgroundTasks, Depends from fastapi import APIRouter, BackgroundTasks, Depends
from freqtrade.configuration.config_validation import validate_config_consistency from freqtrade.configuration.config_validation import validate_config_consistency
from freqtrade.data.btanalysis import get_backtest_resultlist, load_and_merge_backtest_result
from freqtrade.enums import BacktestState from freqtrade.enums import BacktestState
from freqtrade.exceptions import DependencyException from freqtrade.exceptions import DependencyException
from freqtrade.rpc.api_server.api_schemas import BacktestRequest, BacktestResponse from freqtrade.rpc.api_server.api_schemas import (BacktestHistoryEntry, BacktestRequest,
BacktestResponse)
from freqtrade.rpc.api_server.deps import get_config, is_webserver_mode from freqtrade.rpc.api_server.deps import get_config, is_webserver_mode
from freqtrade.rpc.api_server.webserver import ApiServer from freqtrade.rpc.api_server.webserver import ApiServer
from freqtrade.rpc.rpc import RPCException from freqtrade.rpc.rpc import RPCException
@ -200,3 +203,30 @@ def api_backtest_abort(ws_mode=Depends(is_webserver_mode)):
"progress": 0, "progress": 0,
"status_msg": "Backtest ended", "status_msg": "Backtest ended",
} }
@router.get('/backtest/history', response_model=List[BacktestHistoryEntry], tags=['webserver', 'backtest'])
def api_backtest_history(config=Depends(get_config), ws_mode=Depends(is_webserver_mode)):
# Get backtest result history, read from metadata files
return get_backtest_resultlist(config['user_data_dir'] / 'backtest_results')
@router.get('/backtest/history/result', response_model=BacktestResponse, tags=['webserver', 'backtest'])
def api_backtest_history_result(filename: str, strategy: str, config=Depends(get_config), ws_mode=Depends(is_webserver_mode)):
# Get backtest result history, read from metadata files
fn = config['user_data_dir'] / 'backtest_results' / filename
results: Dict[str, Any] = {
'metadata': {},
'strategy': {},
'strategy_comparison': [],
}
load_and_merge_backtest_result(strategy, fn, results)
return {
"status": "ended",
"running": False,
"step": "",
"progress": 1,
"status_msg": "Historic result",
"backtest_result": results,
}

View File

@ -421,6 +421,13 @@ class BacktestResponse(BaseModel):
backtest_result: Optional[Dict[str, Any]] backtest_result: Optional[Dict[str, Any]]
class BacktestHistoryEntry(BaseModel):
filename: str
strategy: str
run_id: str
backtest_start_time: int
class SysInfo(BaseModel): class SysInfo(BaseModel):
cpu_pct: List[float] cpu_pct: List[float]
ram_pct: float ram_pct: float

View File

@ -35,7 +35,8 @@ logger = logging.getLogger(__name__)
# 1.13: forcebuy supports stake_amount # 1.13: forcebuy supports stake_amount
# versions 2.xx -> futures/short branch # versions 2.xx -> futures/short branch
# 2.14: Add entry/exit orders to trade response # 2.14: Add entry/exit orders to trade response
API_VERSION = 2.14 # 2.15: Add backtest history endpoints
API_VERSION = 2.15
# Public API, requires no auth. # Public API, requires no auth.
router_public = APIRouter() router_public = APIRouter()

View File

@ -387,7 +387,7 @@ class Telegram(RPCHandler):
else: else:
return "\N{CROSS MARK}" return "\N{CROSS MARK}"
def _prepare_entry_details(self, filled_orders: List, base_currency: str, is_open: bool): def _prepare_entry_details(self, filled_orders: List, quote_currency: str, is_open: bool):
""" """
Prepare details of trade with entry adjustment enabled Prepare details of trade with entry adjustment enabled
""" """
@ -405,7 +405,7 @@ class Telegram(RPCHandler):
if x == 0: if x == 0:
lines.append(f"*Entry #{x+1}:*") lines.append(f"*Entry #{x+1}:*")
lines.append( lines.append(
f"*Entry Amount:* {cur_entry_amount} ({order['cost']:.8f} {base_currency})") f"*Entry Amount:* {cur_entry_amount} ({order['cost']:.8f} {quote_currency})")
lines.append(f"*Average Entry Price:* {cur_entry_average}") lines.append(f"*Average Entry Price:* {cur_entry_average}")
else: else:
sumA = 0 sumA = 0
@ -419,7 +419,8 @@ class Telegram(RPCHandler):
if prev_avg_price: if prev_avg_price:
minus_on_entry = (cur_entry_average - prev_avg_price) / prev_avg_price minus_on_entry = (cur_entry_average - prev_avg_price) / prev_avg_price
dur_entry = cur_entry_datetime - arrow.get(filled_orders[x-1]["order_filled_date"]) dur_entry = cur_entry_datetime - arrow.get(
filled_orders[x - 1]["order_filled_date"])
days = dur_entry.days days = dur_entry.days
hours, remainder = divmod(dur_entry.seconds, 3600) hours, remainder = divmod(dur_entry.seconds, 3600)
minutes, seconds = divmod(remainder, 60) minutes, seconds = divmod(remainder, 60)
@ -428,7 +429,7 @@ class Telegram(RPCHandler):
lines.append("({})".format(cur_entry_datetime lines.append("({})".format(cur_entry_datetime
.humanize(granularity=["day", "hour", "minute"]))) .humanize(granularity=["day", "hour", "minute"])))
lines.append( lines.append(
f"*Entry Amount:* {cur_entry_amount} ({order['cost']:.8f} {base_currency})") f"*Entry Amount:* {cur_entry_amount} ({order['cost']:.8f} {quote_currency})")
lines.append(f"*Average Entry Price:* {cur_entry_average} " lines.append(f"*Average Entry Price:* {cur_entry_average} "
f"({price_to_1st_entry:.2%} from 1st entry rate)") f"({price_to_1st_entry:.2%} from 1st entry rate)")
lines.append(f"*Order filled at:* {order['order_filled_date']}") lines.append(f"*Order filled at:* {order['order_filled_date']}")
@ -471,7 +472,7 @@ class Telegram(RPCHandler):
"*Current Pair:* {pair}", "*Current Pair:* {pair}",
"*Direction:* " + ("`Short`" if r.get('is_short') else "`Long`"), "*Direction:* " + ("`Short`" if r.get('is_short') else "`Long`"),
"*Leverage:* `{leverage}`" if r.get('leverage') else "", "*Leverage:* `{leverage}`" if r.get('leverage') else "",
"*Amount:* `{amount} ({stake_amount} {base_currency})`", "*Amount:* `{amount} ({stake_amount} {quote_currency})`",
"*Enter Tag:* `{enter_tag}`" if r['enter_tag'] else "", "*Enter Tag:* `{enter_tag}`" if r['enter_tag'] else "",
"*Exit Reason:* `{exit_reason}`" if r['exit_reason'] else "", "*Exit Reason:* `{exit_reason}`" if r['exit_reason'] else "",
] ]

View File

@ -206,18 +206,18 @@ class IStrategy(ABC, HyperStrategyMixin):
""" """
pass pass
def check_buy_timeout(self, pair: str, trade: Trade, order: dict, def check_buy_timeout(self, pair: str, trade: Trade, order: Order,
current_time: datetime, **kwargs) -> bool: current_time: datetime, **kwargs) -> bool:
""" """
DEPRECATED: Please use `check_entry_timeout` instead. DEPRECATED: Please use `check_entry_timeout` instead.
""" """
return False return False
def check_entry_timeout(self, pair: str, trade: Trade, order: dict, def check_entry_timeout(self, pair: str, trade: Trade, order: Order,
current_time: datetime, **kwargs) -> bool: current_time: datetime, **kwargs) -> bool:
""" """
Check entry timeout function callback. Check entry timeout function callback.
This method can be used to override the enter-timeout. This method can be used to override the entry-timeout.
It is called whenever a limit entry order has been created, It is called whenever a limit entry order has been created,
and is not yet fully filled. and is not yet fully filled.
Configuration options in `unfilledtimeout` will be verified before this, Configuration options in `unfilledtimeout` will be verified before this,
@ -225,8 +225,8 @@ class IStrategy(ABC, HyperStrategyMixin):
When not implemented by a strategy, this simply returns False. When not implemented by a strategy, this simply returns False.
:param pair: Pair the trade is for :param pair: Pair the trade is for
:param trade: trade object. :param trade: Trade object.
:param order: Order dictionary as returned from CCXT. :param order: Order object.
:param current_time: datetime object, containing the current datetime :param current_time: datetime object, containing the current datetime
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return bool: When True is returned, then the entry order is cancelled. :return bool: When True is returned, then the entry order is cancelled.
@ -234,30 +234,30 @@ class IStrategy(ABC, HyperStrategyMixin):
return self.check_buy_timeout( return self.check_buy_timeout(
pair=pair, trade=trade, order=order, current_time=current_time) pair=pair, trade=trade, order=order, current_time=current_time)
def check_sell_timeout(self, pair: str, trade: Trade, order: dict, def check_sell_timeout(self, pair: str, trade: Trade, order: Order,
current_time: datetime, **kwargs) -> bool: current_time: datetime, **kwargs) -> bool:
""" """
DEPRECATED: Please use `check_exit_timeout` instead. DEPRECATED: Please use `check_exit_timeout` instead.
""" """
return False return False
def check_exit_timeout(self, pair: str, trade: Trade, order: dict, def check_exit_timeout(self, pair: str, trade: Trade, order: Order,
current_time: datetime, **kwargs) -> bool: current_time: datetime, **kwargs) -> bool:
""" """
Check sell timeout function callback. Check exit timeout function callback.
This method can be used to override the exit-timeout. This method can be used to override the exit-timeout.
It is called whenever a (long) limit sell order or (short) limit buy It is called whenever a limit exit order has been created,
has been created, and is not yet fully filled. and is not yet fully filled.
Configuration options in `unfilledtimeout` will be verified before this, Configuration options in `unfilledtimeout` will be verified before this,
so ensure to set these timeouts high enough. so ensure to set these timeouts high enough.
When not implemented by a strategy, this simply returns False. When not implemented by a strategy, this simply returns False.
:param pair: Pair the trade is for :param pair: Pair the trade is for
:param trade: trade object. :param trade: Trade object.
:param order: Order dictionary as returned from CCXT. :param order: Order object
:param current_time: datetime object, containing the current datetime :param current_time: datetime object, containing the current datetime
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return bool: When True is returned, then the (long)sell/(short)buy-order is cancelled. :return bool: When True is returned, then the exit-order is cancelled.
""" """
return self.check_sell_timeout( return self.check_sell_timeout(
pair=pair, trade=trade, order=order, current_time=current_time) pair=pair, trade=trade, order=order, current_time=current_time)

View File

@ -93,9 +93,9 @@ def stoploss_from_open(
return 1 return 1
if is_short is True: if is_short is True:
stoploss = -1+((1-open_relative_stop)/(1-current_profit)) stoploss = -1 + ((1 - open_relative_stop) / (1 - current_profit))
else: else:
stoploss = 1-((1+open_relative_stop)/(1+current_profit)) stoploss = 1 - ((1 + open_relative_stop) / (1 + current_profit))
# negative stoploss values indicate the requested stop price is higher/lower # negative stoploss values indicate the requested stop price is higher/lower
# (long/short) than the current price # (long/short) than the current price

View File

@ -191,7 +191,8 @@ def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount:
""" """
return True return True
def check_entry_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool: def check_entry_timeout(self, pair: str, trade: 'Trade', order: 'Order',
current_time: datetime, **kwargs) -> bool:
""" """
Check entry timeout function callback. Check entry timeout function callback.
This method can be used to override the entry-timeout. This method can be used to override the entry-timeout.
@ -204,14 +205,16 @@ def check_entry_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs)
When not implemented by a strategy, this simply returns False. When not implemented by a strategy, this simply returns False.
:param pair: Pair the trade is for :param pair: Pair the trade is for
:param trade: trade object. :param trade: Trade object.
:param order: Order dictionary as returned from CCXT. :param order: Order object.
:param current_time: datetime object, containing the current datetime
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return bool: When True is returned, then the buy-order is cancelled. :return bool: When True is returned, then the entry order is cancelled.
""" """
return False return False
def check_exit_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool: def check_exit_timeout(self, pair: str, trade: 'Trade', order: 'Order',
current_time: datetime, **kwargs) -> bool:
""" """
Check exit timeout function callback. Check exit timeout function callback.
This method can be used to override the exit-timeout. This method can be used to override the exit-timeout.
@ -224,8 +227,9 @@ def check_exit_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -
When not implemented by a strategy, this simply returns False. When not implemented by a strategy, this simply returns False.
:param pair: Pair the trade is for :param pair: Pair the trade is for
:param trade: trade object. :param trade: Trade object.
:param order: Order dictionary as returned from CCXT. :param order: Order object.
:param current_time: datetime object, containing the current datetime
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return bool: When True is returned, then the exit-order is cancelled. :return bool: When True is returned, then the exit-order is cancelled.
""" """

View File

@ -39,7 +39,9 @@ console_scripts =
freqtrade = freqtrade.main:main freqtrade = freqtrade.main:main
[flake8] [flake8]
#ignore = # Default from https://flake8.pycqa.org/en/latest/user/options.html#cmdoption-flake8-ignore
# minus E226
ignore = E121,E123,E126,E24,E704,W503,W504
max-line-length = 100 max-line-length = 100
max-complexity = 12 max-complexity = 12
exclude = exclude =

View File

@ -1429,7 +1429,7 @@ def test_backtesting_show(mocker, testdatadir, capsys):
args = [ args = [
"backtesting-show", "backtesting-show",
"--export-filename", "--export-filename",
f"{testdatadir / 'backtest-result_new.json'}", f"{testdatadir / 'backtest_results/backtest-result_new.json'}",
"--show-pair-list" "--show-pair-list"
] ]
pargs = get_args(args) pargs = get_args(args)

View File

@ -27,18 +27,19 @@ def test_get_latest_backtest_filename(testdatadir, mocker):
with pytest.raises(ValueError, with pytest.raises(ValueError,
match=r"Directory .* does not seem to contain .*"): match=r"Directory .* does not seem to contain .*"):
get_latest_backtest_filename(testdatadir.parent) get_latest_backtest_filename(testdatadir)
res = get_latest_backtest_filename(testdatadir) testdir_bt = testdatadir / "backtest_results"
res = get_latest_backtest_filename(testdir_bt)
assert res == 'backtest-result_new.json' assert res == 'backtest-result_new.json'
res = get_latest_backtest_filename(str(testdatadir)) res = get_latest_backtest_filename(str(testdir_bt))
assert res == 'backtest-result_new.json' assert res == 'backtest-result_new.json'
mocker.patch("freqtrade.data.btanalysis.json_load", return_value={}) mocker.patch("freqtrade.data.btanalysis.json_load", return_value={})
with pytest.raises(ValueError, match=r"Invalid '.last_result.json' format."): with pytest.raises(ValueError, match=r"Invalid '.last_result.json' format."):
get_latest_backtest_filename(testdatadir) get_latest_backtest_filename(testdir_bt)
def test_get_latest_hyperopt_file(testdatadir): def test_get_latest_hyperopt_file(testdatadir):
@ -81,7 +82,7 @@ def test_load_backtest_data_old_format(testdatadir, mocker):
def test_load_backtest_data_new_format(testdatadir): def test_load_backtest_data_new_format(testdatadir):
filename = testdatadir / "backtest-result_new.json" filename = testdatadir / "backtest_results/backtest-result_new.json"
bt_data = load_backtest_data(filename) bt_data = load_backtest_data(filename)
assert isinstance(bt_data, DataFrame) assert isinstance(bt_data, DataFrame)
assert set(bt_data.columns) == set(BT_DATA_COLUMNS + ['close_timestamp', 'open_timestamp']) assert set(bt_data.columns) == set(BT_DATA_COLUMNS + ['close_timestamp', 'open_timestamp'])
@ -92,19 +93,19 @@ def test_load_backtest_data_new_format(testdatadir):
assert bt_data.equals(bt_data2) assert bt_data.equals(bt_data2)
# Test loading from folder (must yield same result) # Test loading from folder (must yield same result)
bt_data3 = load_backtest_data(testdatadir) bt_data3 = load_backtest_data(testdatadir / "backtest_results")
assert bt_data.equals(bt_data3) assert bt_data.equals(bt_data3)
with pytest.raises(ValueError, match=r"File .* does not exist\."): with pytest.raises(ValueError, match=r"File .* does not exist\."):
load_backtest_data(str("filename") + "nofile") load_backtest_data(str("filename") + "nofile")
with pytest.raises(ValueError, match=r"Unknown dataformat."): with pytest.raises(ValueError, match=r"Unknown dataformat."):
load_backtest_data(testdatadir / LAST_BT_RESULT_FN) load_backtest_data(testdatadir / "backtest_results" / LAST_BT_RESULT_FN)
def test_load_backtest_data_multi(testdatadir): def test_load_backtest_data_multi(testdatadir):
filename = testdatadir / "backtest-result_multistrat.json" filename = testdatadir / "backtest_results/backtest-result_multistrat.json"
for strategy in ('StrategyTestV2', 'TestStrategy'): for strategy in ('StrategyTestV2', 'TestStrategy'):
bt_data = load_backtest_data(filename, strategy=strategy) bt_data = load_backtest_data(filename, strategy=strategy)
assert isinstance(bt_data, DataFrame) assert isinstance(bt_data, DataFrame)
@ -182,7 +183,7 @@ def test_extract_trades_of_period(testdatadir):
def test_analyze_trade_parallelism(testdatadir): def test_analyze_trade_parallelism(testdatadir):
filename = testdatadir / "backtest-result_new.json" filename = testdatadir / "backtest_results/backtest-result_new.json"
bt_data = load_backtest_data(filename) bt_data = load_backtest_data(filename)
res = analyze_trade_parallelism(bt_data, "5m") res = analyze_trade_parallelism(bt_data, "5m")
@ -256,7 +257,7 @@ def test_combine_dataframes_with_mean_no_data(testdatadir):
def test_create_cum_profit(testdatadir): def test_create_cum_profit(testdatadir):
filename = testdatadir / "backtest-result_new.json" filename = testdatadir / "backtest_results/backtest-result_new.json"
bt_data = load_backtest_data(filename) bt_data = load_backtest_data(filename)
timerange = TimeRange.parse_timerange("20180110-20180112") timerange = TimeRange.parse_timerange("20180110-20180112")
@ -272,7 +273,7 @@ def test_create_cum_profit(testdatadir):
def test_create_cum_profit1(testdatadir): def test_create_cum_profit1(testdatadir):
filename = testdatadir / "backtest-result_new.json" filename = testdatadir / "backtest_results/backtest-result_new.json"
bt_data = load_backtest_data(filename) bt_data = load_backtest_data(filename)
# Move close-time to "off" the candle, to make sure the logic still works # Move close-time to "off" the candle, to make sure the logic still works
bt_data.loc[:, 'close_date'] = bt_data.loc[:, 'close_date'] + DateOffset(seconds=20) bt_data.loc[:, 'close_date'] = bt_data.loc[:, 'close_date'] + DateOffset(seconds=20)
@ -294,7 +295,7 @@ def test_create_cum_profit1(testdatadir):
def test_calculate_max_drawdown(testdatadir): def test_calculate_max_drawdown(testdatadir):
filename = testdatadir / "backtest-result_new.json" filename = testdatadir / "backtest_results/backtest-result_new.json"
bt_data = load_backtest_data(filename) bt_data = load_backtest_data(filename)
_, hdate, lowdate, hval, lval, drawdown = calculate_max_drawdown( _, hdate, lowdate, hval, lval, drawdown = calculate_max_drawdown(
bt_data, value_col="profit_abs") bt_data, value_col="profit_abs")
@ -318,7 +319,7 @@ def test_calculate_max_drawdown(testdatadir):
def test_calculate_csum(testdatadir): def test_calculate_csum(testdatadir):
filename = testdatadir / "backtest-result_new.json" filename = testdatadir / "backtest_results/backtest-result_new.json"
bt_data = load_backtest_data(filename) bt_data = load_backtest_data(filename)
csum_min, csum_max = calculate_csum(bt_data) csum_min, csum_max = calculate_csum(bt_data)

View File

@ -231,6 +231,10 @@ def test_validate_order_time_in_force(default_conf, mocker, caplog):
(2.34559, 2, 3, 1, 2.345, 'spot'), (2.34559, 2, 3, 1, 2.345, 'spot'),
(2.9999, 2, 3, 1, 2.999, 'spot'), (2.9999, 2, 3, 1, 2.999, 'spot'),
(2.9909, 2, 3, 1, 2.990, 'spot'), (2.9909, 2, 3, 1, 2.990, 'spot'),
(2.9909, 2, 0, 1, 2, 'spot'),
(29991.5555, 2, 0, 1, 29991, 'spot'),
(29991.5555, 2, -1, 1, 29990, 'spot'),
(29991.5555, 2, -2, 1, 29900, 'spot'),
# Tests for Tick-size # Tests for Tick-size
(2.34559, 4, 0.0001, 1, 2.3455, 'spot'), (2.34559, 4, 0.0001, 1, 2.3455, 'spot'),
(2.34559, 4, 0.00001, 1, 2.34559, 'spot'), (2.34559, 4, 0.00001, 1, 2.34559, 'spot'),
@ -382,11 +386,11 @@ def test__get_stake_amount_limit(mocker, default_conf) -> None:
) )
# min # min
result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss) result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss)
expected_result = 2 * (1+0.05) / (1-abs(stoploss)) expected_result = 2 * (1 + 0.05) / (1 - abs(stoploss))
assert isclose(result, expected_result) assert isclose(result, expected_result)
# With Leverage # With Leverage
result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss, 3.0) result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss, 3.0)
assert isclose(result, expected_result/3) assert isclose(result, expected_result / 3)
# max # max
result = exchange.get_max_pair_stake_amount('ETH/BTC', 2) result = exchange.get_max_pair_stake_amount('ETH/BTC', 2)
assert result == 10000 assert result == 10000
@ -401,11 +405,11 @@ def test__get_stake_amount_limit(mocker, default_conf) -> None:
PropertyMock(return_value=markets) PropertyMock(return_value=markets)
) )
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss)
expected_result = 2 * 2 * (1+0.05) / (1-abs(stoploss)) expected_result = 2 * 2 * (1 + 0.05) / (1 - abs(stoploss))
assert isclose(result, expected_result) assert isclose(result, expected_result)
# With Leverage # With Leverage
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 5.0) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 5.0)
assert isclose(result, expected_result/5) assert isclose(result, expected_result / 5)
# max # max
result = exchange.get_max_pair_stake_amount('ETH/BTC', 2) result = exchange.get_max_pair_stake_amount('ETH/BTC', 2)
assert result == 20000 assert result == 20000
@ -420,11 +424,11 @@ def test__get_stake_amount_limit(mocker, default_conf) -> None:
PropertyMock(return_value=markets) PropertyMock(return_value=markets)
) )
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss)
expected_result = max(2, 2 * 2) * (1+0.05) / (1-abs(stoploss)) expected_result = max(2, 2 * 2) * (1 + 0.05) / (1 - abs(stoploss))
assert isclose(result, expected_result) assert isclose(result, expected_result)
# With Leverage # With Leverage
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 10) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 10)
assert isclose(result, expected_result/10) assert isclose(result, expected_result / 10)
# min amount and cost are set (amount is minial) # min amount and cost are set (amount is minial)
markets["ETH/BTC"]["limits"] = { markets["ETH/BTC"]["limits"] = {
@ -436,11 +440,11 @@ def test__get_stake_amount_limit(mocker, default_conf) -> None:
PropertyMock(return_value=markets) PropertyMock(return_value=markets)
) )
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss)
expected_result = max(8, 2 * 2) * (1+0.05) / (1-abs(stoploss)) expected_result = max(8, 2 * 2) * (1 + 0.05) / (1 - abs(stoploss))
assert isclose(result, expected_result) assert isclose(result, expected_result)
# With Leverage # With Leverage
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 7.0) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 7.0)
assert isclose(result, expected_result/7.0) assert isclose(result, expected_result / 7.0)
# Max # Max
result = exchange.get_max_pair_stake_amount('ETH/BTC', 2) result = exchange.get_max_pair_stake_amount('ETH/BTC', 2)
assert result == 1000 assert result == 1000
@ -450,7 +454,7 @@ def test__get_stake_amount_limit(mocker, default_conf) -> None:
assert isclose(result, expected_result) assert isclose(result, expected_result)
# With Leverage # With Leverage
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -0.4, 8.0) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -0.4, 8.0)
assert isclose(result, expected_result/8.0) assert isclose(result, expected_result / 8.0)
# Max # Max
result = exchange.get_max_pair_stake_amount('ETH/BTC', 2) result = exchange.get_max_pair_stake_amount('ETH/BTC', 2)
assert result == 1000 assert result == 1000
@ -461,7 +465,7 @@ def test__get_stake_amount_limit(mocker, default_conf) -> None:
assert isclose(result, expected_result) assert isclose(result, expected_result)
# With Leverage # With Leverage
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1, 12.0) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1, 12.0)
assert isclose(result, expected_result/12) assert isclose(result, expected_result / 12)
# Max # Max
result = exchange.get_max_pair_stake_amount('ETH/BTC', 2) result = exchange.get_max_pair_stake_amount('ETH/BTC', 2)
assert result == 1000 assert result == 1000
@ -489,7 +493,7 @@ def test__get_stake_amount_limit(mocker, default_conf) -> None:
) )
# With Leverage, Contract size 10 # With Leverage, Contract size 10
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1, 12.0) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1, 12.0)
assert isclose(result, (expected_result/12) * 10.0) assert isclose(result, (expected_result / 12) * 10.0)
# Max # Max
result = exchange.get_max_pair_stake_amount('ETH/BTC', 2) result = exchange.get_max_pair_stake_amount('ETH/BTC', 2)
assert result == 10000 assert result == 10000
@ -510,7 +514,7 @@ def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None:
PropertyMock(return_value=markets) PropertyMock(return_value=markets)
) )
result = exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss) result = exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss)
expected_result = max(0.0001, 0.001 * 0.020405) * (1+0.05) / (1-abs(stoploss)) expected_result = max(0.0001, 0.001 * 0.020405) * (1 + 0.05) / (1 - abs(stoploss))
assert round(result, 8) == round(expected_result, 8) assert round(result, 8) == round(expected_result, 8)
# Max # Max
result = exchange.get_max_pair_stake_amount('ETH/BTC', 2.0) result = exchange.get_max_pair_stake_amount('ETH/BTC', 2.0)
@ -518,12 +522,12 @@ def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None:
# Leverage # Leverage
result = exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss, 3.0) result = exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss, 3.0)
assert round(result, 8) == round(expected_result/3, 8) assert round(result, 8) == round(expected_result / 3, 8)
# Contract_size # Contract_size
markets["ETH/BTC"]["contractSize"] = 0.1 markets["ETH/BTC"]["contractSize"] = 0.1
result = exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss, 3.0) result = exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss, 3.0)
assert round(result, 8) == round((expected_result/3), 8) assert round(result, 8) == round((expected_result / 3), 8)
# Max # Max
result = exchange.get_max_pair_stake_amount('ETH/BTC', 12.0) result = exchange.get_max_pair_stake_amount('ETH/BTC', 12.0)
@ -2691,9 +2695,10 @@ async def test__async_get_trade_history_time(default_conf, mocker, caplog, excha
# Monkey-patch async function # Monkey-patch async function
exchange._api_async.fetch_trades = MagicMock(side_effect=mock_get_trade_hist) exchange._api_async.fetch_trades = MagicMock(side_effect=mock_get_trade_hist)
pair = 'ETH/BTC' pair = 'ETH/BTC'
ret = await exchange._async_get_trade_history_time(pair, ret = await exchange._async_get_trade_history_time(
pair,
since=fetch_trades_result[0]['timestamp'], since=fetch_trades_result[0]['timestamp'],
until=fetch_trades_result[-1]['timestamp']-1) until=fetch_trades_result[-1]['timestamp'] - 1)
assert type(ret) is tuple assert type(ret) is tuple
assert ret[0] == pair assert ret[0] == pair
assert type(ret[1]) is list assert type(ret[1]) is list
@ -2729,7 +2734,7 @@ async def test__async_get_trade_history_time_empty(default_conf, mocker, caplog,
exchange._async_fetch_trades = MagicMock(side_effect=mock_get_trade_hist) exchange._async_fetch_trades = MagicMock(side_effect=mock_get_trade_hist)
pair = 'ETH/BTC' pair = 'ETH/BTC'
ret = await exchange._async_get_trade_history_time(pair, since=trades_history[0][0], ret = await exchange._async_get_trade_history_time(pair, since=trades_history[0][0],
until=trades_history[-1][0]-1) until=trades_history[-1][0] - 1)
assert type(ret) is tuple assert type(ret) is tuple
assert ret[0] == pair assert ret[0] == pair
assert type(ret[1]) is list assert type(ret[1]) is list

View File

@ -6,7 +6,7 @@ import pytest
from freqtrade.leverage import interest from freqtrade.leverage import interest
ten_mins = Decimal(1/6) ten_mins = Decimal(1 / 6)
five_hours = Decimal(5.0) five_hours = Decimal(5.0)
twentyfive_hours = Decimal(25.0) twentyfive_hours = Decimal(25.0)

View File

@ -190,7 +190,7 @@ def test_store_backtest_stats(testdatadir, mocker):
assert dump_mock.call_count == 3 assert dump_mock.call_count == 3
assert isinstance(dump_mock.call_args_list[0][0][0], Path) assert isinstance(dump_mock.call_args_list[0][0][0], Path)
assert str(dump_mock.call_args_list[0][0][0]).startswith(str(testdatadir/'backtest-result')) assert str(dump_mock.call_args_list[0][0][0]).startswith(str(testdatadir / 'backtest-result'))
dump_mock.reset_mock() dump_mock.reset_mock()
filename = testdatadir / 'testresult.json' filename = testdatadir / 'testresult.json'
@ -228,7 +228,7 @@ def test_generate_pair_metrics():
def test_generate_daily_stats(testdatadir): def test_generate_daily_stats(testdatadir):
filename = testdatadir / "backtest-result_new.json" filename = testdatadir / "backtest_results/backtest-result_new.json"
bt_data = load_backtest_data(filename) bt_data = load_backtest_data(filename)
res = generate_daily_stats(bt_data) res = generate_daily_stats(bt_data)
assert isinstance(res, dict) assert isinstance(res, dict)
@ -248,7 +248,7 @@ def test_generate_daily_stats(testdatadir):
def test_generate_trading_stats(testdatadir): def test_generate_trading_stats(testdatadir):
filename = testdatadir / "backtest-result_new.json" filename = testdatadir / "backtest_results/backtest-result_new.json"
bt_data = load_backtest_data(filename) bt_data = load_backtest_data(filename)
res = generate_trading_stats(bt_data) res = generate_trading_stats(bt_data)
assert isinstance(res, dict) assert isinstance(res, dict)
@ -332,7 +332,7 @@ def test_generate_sell_reason_stats():
def test_text_table_strategy(testdatadir): def test_text_table_strategy(testdatadir):
filename = testdatadir / "backtest-result_multistrat.json" filename = testdatadir / "backtest_results/backtest-result_multistrat.json"
bt_res_data = load_backtest_stats(filename) bt_res_data = load_backtest_stats(filename)
bt_res_data_comparison = bt_res_data.pop('strategy_comparison') bt_res_data_comparison = bt_res_data.pop('strategy_comparison')
@ -364,7 +364,7 @@ def test_generate_edge_table():
def test_generate_periodic_breakdown_stats(testdatadir): def test_generate_periodic_breakdown_stats(testdatadir):
filename = testdatadir / "backtest-result_new.json" filename = testdatadir / "backtest_results/backtest-result_new.json"
bt_data = load_backtest_data(filename).to_dict(orient='records') bt_data = load_backtest_data(filename).to_dict(orient='records')
res = generate_periodic_breakdown_stats(bt_data, 'day') res = generate_periodic_breakdown_stats(bt_data, 'day')
@ -392,7 +392,7 @@ def test__get_resample_from_period():
def test_show_sorted_pairlist(testdatadir, default_conf, capsys): def test_show_sorted_pairlist(testdatadir, default_conf, capsys):
filename = testdatadir / "backtest-result_new.json" filename = testdatadir / "backtest_results/backtest-result_new.json"
bt_data = load_backtest_stats(filename) bt_data = load_backtest_stats(filename)
default_conf['backtest_show_pair_list'] = True default_conf['backtest_show_pair_list'] = True

View File

@ -1581,6 +1581,38 @@ def test_api_backtesting(botclient, mocker, fee, caplog, tmpdir):
assert result['status_msg'] == 'Backtest reset' assert result['status_msg'] == 'Backtest reset'
def test_api_backtest_history(botclient, mocker, testdatadir):
ftbot, client = botclient
mocker.patch('freqtrade.data.btanalysis._get_backtest_files',
return_value=[
testdatadir / 'backtest_results/backtest-result_multistrat.json',
testdatadir / 'backtest_results/backtest-result_new.json'
])
rc = client_get(client, f"{BASE_URI}/backtest/history")
assert_response(rc, 502)
ftbot.config['user_data_dir'] = testdatadir
ftbot.config['runmode'] = RunMode.WEBSERVER
rc = client_get(client, f"{BASE_URI}/backtest/history")
assert_response(rc)
result = rc.json()
assert len(result) == 3
fn = result[0]['filename']
assert fn == "backtest-result_multistrat.json"
strategy = result[0]['strategy']
rc = client_get(client, f"{BASE_URI}/backtest/history/result?filename={fn}&strategy={strategy}")
assert_response(rc)
result2 = rc.json()
assert result2
assert result2['status'] == 'ended'
assert not result2['running']
assert result2['progress'] == 1
# Only one strategy loaded - even though we use multiresult
assert len(result2['backtest_result']['strategy']) == 1
assert result2['backtest_result']['strategy'][strategy]
def test_health(botclient): def test_health(botclient):
ftbot, client = botclient ftbot, client = botclient

View File

@ -164,7 +164,7 @@ def test_stoploss_from_absolute():
assert pytest.approx(stoploss_from_absolute(90, 100, True)) == 0 assert pytest.approx(stoploss_from_absolute(90, 100, True)) == 0
assert pytest.approx(stoploss_from_absolute(100, 100, True)) == 0 assert pytest.approx(stoploss_from_absolute(100, 100, True)) == 0
assert pytest.approx(stoploss_from_absolute(110, 100, True)) == -(1 - (110/100)) assert pytest.approx(stoploss_from_absolute(110, 100, True)) == -(1 - (110 / 100))
assert pytest.approx(stoploss_from_absolute(110, 100, True)) == 0.1 assert pytest.approx(stoploss_from_absolute(110, 100, True)) == 0.1
assert pytest.approx(stoploss_from_absolute(105, 100, True)) == 0.05 assert pytest.approx(stoploss_from_absolute(105, 100, True)) == 0.05
assert pytest.approx(stoploss_from_absolute(100, 0, True)) == 1 assert pytest.approx(stoploss_from_absolute(100, 0, True)) == 1

View File

@ -119,7 +119,7 @@ def test_set_stop_loss_isolated_liq(fee):
assert trade.stop_loss is None assert trade.stop_loss is None
assert trade.initial_stop_loss is None assert trade.initial_stop_loss is None
trade._set_stop_loss(0.1, (1.0/9.0)) trade._set_stop_loss(0.1, (1.0 / 9.0))
assert trade.liquidation_price == 0.09 assert trade.liquidation_price == 0.09
assert trade.stop_loss == 0.1 assert trade.stop_loss == 0.1
assert trade.initial_stop_loss == 0.1 assert trade.initial_stop_loss == 0.1
@ -160,7 +160,7 @@ def test_set_stop_loss_isolated_liq(fee):
assert trade.stop_loss is None assert trade.stop_loss is None
assert trade.initial_stop_loss is None assert trade.initial_stop_loss is None
trade._set_stop_loss(0.08, (1.0/9.0)) trade._set_stop_loss(0.08, (1.0 / 9.0))
assert trade.liquidation_price == 0.09 assert trade.liquidation_price == 0.09
assert trade.stop_loss == 0.08 assert trade.stop_loss == 0.08
assert trade.initial_stop_loss == 0.08 assert trade.initial_stop_loss == 0.08
@ -171,13 +171,13 @@ def test_set_stop_loss_isolated_liq(fee):
assert trade.initial_stop_loss == 0.08 assert trade.initial_stop_loss == 0.08
trade.set_isolated_liq(0.07) trade.set_isolated_liq(0.07)
trade._set_stop_loss(0.1, (1.0/8.0)) trade._set_stop_loss(0.1, (1.0 / 8.0))
assert trade.liquidation_price == 0.07 assert trade.liquidation_price == 0.07
assert trade.stop_loss == 0.07 assert trade.stop_loss == 0.07
assert trade.initial_stop_loss == 0.08 assert trade.initial_stop_loss == 0.08
# Stop doesn't move stop higher # Stop doesn't move stop higher
trade._set_stop_loss(0.1, (1.0/9.0)) trade._set_stop_loss(0.1, (1.0 / 9.0))
assert trade.liquidation_price == 0.07 assert trade.liquidation_price == 0.07
assert trade.stop_loss == 0.07 assert trade.stop_loss == 0.07
assert trade.initial_stop_loss == 0.08 assert trade.initial_stop_loss == 0.08

View File

@ -157,7 +157,7 @@ def test_plot_trades(testdatadir, caplog):
assert fig == fig1 assert fig == fig1
assert log_has("No trades found.", caplog) assert log_has("No trades found.", caplog)
pair = "ADA/BTC" pair = "ADA/BTC"
filename = testdatadir / "backtest-result_new.json" filename = testdatadir / "backtest_results/backtest-result_new.json"
trades = load_backtest_data(filename) trades = load_backtest_data(filename)
trades = trades.loc[trades['pair'] == pair] trades = trades.loc[trades['pair'] == pair]
@ -298,7 +298,7 @@ def test_generate_plot_file(mocker, caplog):
def test_add_profit(testdatadir): def test_add_profit(testdatadir):
filename = testdatadir / "backtest-result_new.json" filename = testdatadir / "backtest_results/backtest-result_new.json"
bt_data = load_backtest_data(filename) bt_data = load_backtest_data(filename)
timerange = TimeRange.parse_timerange("20180110-20180112") timerange = TimeRange.parse_timerange("20180110-20180112")
@ -318,7 +318,7 @@ def test_add_profit(testdatadir):
def test_generate_profit_graph(testdatadir): def test_generate_profit_graph(testdatadir):
filename = testdatadir / "backtest-result_new.json" filename = testdatadir / "backtest_results/backtest-result_new.json"
trades = load_backtest_data(filename) trades = load_backtest_data(filename)
timerange = TimeRange.parse_timerange("20180110-20180112") timerange = TimeRange.parse_timerange("20180110-20180112")
pairs = ["TRX/BTC", "XLM/BTC"] pairs = ["TRX/BTC", "XLM/BTC"]
@ -456,7 +456,7 @@ def test_plot_profit(default_conf, mocker, testdatadir):
match=r"No trades found, cannot generate Profit-plot.*"): match=r"No trades found, cannot generate Profit-plot.*"):
plot_profit(default_conf) plot_profit(default_conf)
default_conf['exportfilename'] = testdatadir / "backtest-result_new.json" default_conf['exportfilename'] = testdatadir / "backtest_results/backtest-result_new.json"
plot_profit(default_conf) plot_profit(default_conf)

View File

@ -0,0 +1,10 @@
{
"StrategyTestV2": {
"run_id": "430d0271075ef327edbb23088f4db4ebe51a3dbf",
"backtest_start_time": 1648904006
},
"TestStrategy": {
"run_id": "110d0271075ef327edbb23085102b4ebe51a3d55",
"backtest_start_time": 1648904006
}
}

View File

@ -0,0 +1,6 @@
{
"StrategyTestV3": {
"run_id": "430d0271075ef327edbb23088f4db4ebe51a3dbf",
"backtest_start_time": 1648904006
}
}