Merge branch 'develop' into feat_readjust_entry
This commit is contained in:
@@ -16,4 +16,4 @@ class PeriodicCache(TTLCache):
|
||||
return ts - 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)
|
||||
|
@@ -31,7 +31,7 @@ def log_config_error_range(path: str, errmsg: str) -> str:
|
||||
offset = int(offsetlist[0])
|
||||
text = Path(path).read_text()
|
||||
# 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')
|
||||
if len(segments) > 3:
|
||||
# Remove first and last lines, to avoid odd truncations
|
||||
|
@@ -149,7 +149,14 @@ def load_backtest_stats(filename: Union[Path, str]) -> Dict[str, Any]:
|
||||
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)
|
||||
for k in ('metadata', 'strategy'):
|
||||
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
|
||||
|
||||
|
||||
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],
|
||||
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.
|
||||
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)
|
||||
if not metadata:
|
||||
# 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:
|
||||
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:
|
||||
break
|
||||
|
@@ -651,7 +651,7 @@ class Exchange:
|
||||
Re-implementation of ccxt internal methods - ensuring we can test the result is correct
|
||||
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,
|
||||
precision=self.markets[pair]['precision']['amount'],
|
||||
counting_mode=self.precisionMode,
|
||||
@@ -2177,7 +2177,7 @@ class Exchange:
|
||||
lev = tier['lev']
|
||||
|
||||
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']
|
||||
if next_floor > stake_amount: # Next tier min too high for stake amount
|
||||
return min((tier['max'] / stake_amount), lev)
|
||||
|
@@ -31,13 +31,13 @@ def interest(
|
||||
"""
|
||||
exchange_name = exchange_name.lower()
|
||||
if exchange_name == "binance":
|
||||
return borrowed * rate * ceil(hours)/twenty_four
|
||||
return borrowed * rate * ceil(hours) / twenty_four
|
||||
elif exchange_name == "kraken":
|
||||
# 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":
|
||||
# As Explained under #Interest rates section in
|
||||
# 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:
|
||||
raise OperationalException(f"Leverage not available on {exchange_name} with freqtrade")
|
||||
|
@@ -126,7 +126,7 @@ def format_ms_time(date: int) -> str:
|
||||
convert MS date to readable format.
|
||||
: 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):
|
||||
|
@@ -390,8 +390,8 @@ class HyperoptTools():
|
||||
lambda x: '{} {}'.format(
|
||||
round_coin_value(x['Total profit'], stake_currency, keep_trailing_zeros=True),
|
||||
f"({x['Profit']:,.2%})".rjust(10, ' ')
|
||||
).rjust(25+len(stake_currency))
|
||||
if x['Total profit'] != 0.0 else '--'.rjust(25+len(stake_currency)),
|
||||
).rjust(25 + len(stake_currency))
|
||||
if x['Total profit'] != 0.0 else '--'.rjust(25 + len(stake_currency)),
|
||||
axis=1
|
||||
)
|
||||
trials = trials.drop(columns=['Total profit'])
|
||||
@@ -399,11 +399,11 @@ class HyperoptTools():
|
||||
if print_colorized:
|
||||
for i in range(len(trials)):
|
||||
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,
|
||||
str(trials.loc[i][j]), Fore.RESET)
|
||||
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,
|
||||
str(trials.loc[i][j]), Style.RESET_ALL)
|
||||
|
||||
@@ -459,7 +459,7 @@ class HyperoptTools():
|
||||
'loss', 'is_initial_point', 'is_best']
|
||||
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]
|
||||
|
||||
base_columns = ['Best', 'Epoch', 'Trades', 'Avg profit', 'Median profit', 'Total profit',
|
||||
|
@@ -360,7 +360,7 @@ class LocalTrade():
|
||||
if self.has_no_leverage:
|
||||
return 0.0
|
||||
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:
|
||||
return self.amount
|
||||
|
||||
@@ -747,7 +747,7 @@ class LocalTrade():
|
||||
now = (self.close_date or datetime.now(timezone.utc)).replace(tzinfo=None)
|
||||
sec_per_hour = Decimal(3600)
|
||||
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)
|
||||
borrowed = Decimal(self.borrowed)
|
||||
@@ -861,9 +861,9 @@ class LocalTrade():
|
||||
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 / self.open_trade_value)) * leverage
|
||||
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}")
|
||||
|
||||
|
@@ -107,7 +107,7 @@ class VolatilityFilter(IPairList):
|
||||
returns = (np.log(daily_candles.close / daily_candles.close.shift(-1)))
|
||||
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()
|
||||
|
||||
if self._min_volatility <= volatility_avg <= self._max_volatility:
|
||||
|
@@ -1,13 +1,16 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends
|
||||
|
||||
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.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.webserver import ApiServer
|
||||
from freqtrade.rpc.rpc import RPCException
|
||||
@@ -200,3 +203,30 @@ def api_backtest_abort(ws_mode=Depends(is_webserver_mode)):
|
||||
"progress": 0,
|
||||
"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,
|
||||
}
|
||||
|
@@ -421,6 +421,13 @@ class BacktestResponse(BaseModel):
|
||||
backtest_result: Optional[Dict[str, Any]]
|
||||
|
||||
|
||||
class BacktestHistoryEntry(BaseModel):
|
||||
filename: str
|
||||
strategy: str
|
||||
run_id: str
|
||||
backtest_start_time: int
|
||||
|
||||
|
||||
class SysInfo(BaseModel):
|
||||
cpu_pct: List[float]
|
||||
ram_pct: float
|
||||
|
@@ -35,7 +35,8 @@ logger = logging.getLogger(__name__)
|
||||
# 1.13: forcebuy supports stake_amount
|
||||
# versions 2.xx -> futures/short branch
|
||||
# 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.
|
||||
router_public = APIRouter()
|
||||
|
@@ -387,7 +387,7 @@ class Telegram(RPCHandler):
|
||||
else:
|
||||
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
|
||||
"""
|
||||
@@ -405,7 +405,7 @@ class Telegram(RPCHandler):
|
||||
if x == 0:
|
||||
lines.append(f"*Entry #{x+1}:*")
|
||||
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}")
|
||||
else:
|
||||
sumA = 0
|
||||
@@ -419,7 +419,8 @@ class Telegram(RPCHandler):
|
||||
if 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
|
||||
hours, remainder = divmod(dur_entry.seconds, 3600)
|
||||
minutes, seconds = divmod(remainder, 60)
|
||||
@@ -428,7 +429,7 @@ class Telegram(RPCHandler):
|
||||
lines.append("({})".format(cur_entry_datetime
|
||||
.humanize(granularity=["day", "hour", "minute"])))
|
||||
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} "
|
||||
f"({price_to_1st_entry:.2%} from 1st entry rate)")
|
||||
lines.append(f"*Order filled at:* {order['order_filled_date']}")
|
||||
@@ -471,7 +472,7 @@ class Telegram(RPCHandler):
|
||||
"*Current Pair:* {pair}",
|
||||
"*Direction:* " + ("`Short`" if r.get('is_short') else "`Long`"),
|
||||
"*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 "",
|
||||
"*Exit Reason:* `{exit_reason}`" if r['exit_reason'] else "",
|
||||
]
|
||||
|
@@ -206,18 +206,18 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
"""
|
||||
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:
|
||||
"""
|
||||
DEPRECATED: Please use `check_entry_timeout` instead.
|
||||
"""
|
||||
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:
|
||||
"""
|
||||
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,
|
||||
and is not yet fully filled.
|
||||
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.
|
||||
:param pair: Pair the trade is for
|
||||
:param trade: trade object.
|
||||
:param order: Order dictionary as returned from CCXT.
|
||||
:param trade: Trade object.
|
||||
: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.
|
||||
: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(
|
||||
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:
|
||||
"""
|
||||
DEPRECATED: Please use `check_exit_timeout` instead.
|
||||
"""
|
||||
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:
|
||||
"""
|
||||
Check sell timeout function callback.
|
||||
Check exit timeout function callback.
|
||||
This method can be used to override the exit-timeout.
|
||||
It is called whenever a (long) limit sell order or (short) limit buy
|
||||
has been created, and is not yet fully filled.
|
||||
It is called whenever a limit exit order has been created,
|
||||
and is not yet fully filled.
|
||||
Configuration options in `unfilledtimeout` will be verified before this,
|
||||
so ensure to set these timeouts high enough.
|
||||
|
||||
When not implemented by a strategy, this simply returns False.
|
||||
:param pair: Pair the trade is for
|
||||
:param trade: trade object.
|
||||
:param order: Order dictionary as returned from CCXT.
|
||||
:param trade: Trade object.
|
||||
: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.
|
||||
: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(
|
||||
pair=pair, trade=trade, order=order, current_time=current_time)
|
||||
|
@@ -93,9 +93,9 @@ def stoploss_from_open(
|
||||
return 1
|
||||
|
||||
if is_short is True:
|
||||
stoploss = -1+((1-open_relative_stop)/(1-current_profit))
|
||||
stoploss = -1 + ((1 - open_relative_stop) / (1 - current_profit))
|
||||
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
|
||||
# (long/short) than the current price
|
||||
|
@@ -191,7 +191,8 @@ def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount:
|
||||
"""
|
||||
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.
|
||||
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.
|
||||
:param pair: Pair the trade is for
|
||||
:param trade: trade object.
|
||||
:param order: Order dictionary as returned from CCXT.
|
||||
:param trade: Trade object.
|
||||
: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.
|
||||
: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
|
||||
|
||||
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.
|
||||
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.
|
||||
:param pair: Pair the trade is for
|
||||
:param trade: trade object.
|
||||
:param order: Order dictionary as returned from CCXT.
|
||||
:param trade: Trade object.
|
||||
: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.
|
||||
:return bool: When True is returned, then the exit-order is cancelled.
|
||||
"""
|
||||
|
Reference in New Issue
Block a user