Merge branch 'freqtrade:develop' into bt-metrics

This commit is contained in:
Stefano Ariestasia
2023-01-06 08:04:00 +08:00
committed by GitHub
16 changed files with 106 additions and 162 deletions

View File

@@ -52,7 +52,7 @@ def _process_candles_and_indicators(pairlist, strategy_name, trades, signal_cand
return analysed_trades_dict
def _analyze_candles_and_indicators(pair, trades, signal_candles):
def _analyze_candles_and_indicators(pair, trades: pd.DataFrame, signal_candles: pd.DataFrame):
buyf = signal_candles
if len(buyf) > 0:
@@ -120,7 +120,7 @@ def _do_group_table_output(bigdf, glist):
else:
agg_mask = {'profit_abs': ['count', 'sum', 'median', 'mean'],
'profit_ratio': ['sum', 'median', 'mean']}
'profit_ratio': ['median', 'mean', 'sum']}
agg_cols = ['num_buys', 'profit_abs_sum', 'profit_abs_median',
'profit_abs_mean', 'median_profit_pct', 'mean_profit_pct',
'total_profit_pct']

View File

@@ -239,7 +239,7 @@ def calculate_sortino(trades: pd.DataFrame, min_date: datetime, max_date: dateti
down_stdev = np.std(trades.loc[trades['profit_abs'] < 0, 'profit_abs'] / starting_balance)
if down_stdev != 0:
if down_stdev != 0 and not np.isnan(down_stdev):
sortino_ratio = expected_returns_mean / down_stdev * np.sqrt(365)
else:
# Define high (negative) sortino ratio to be clear that this is NOT optimal.

View File

@@ -11,7 +11,7 @@ from freqtrade.enums import CandleType, MarginMode, TradingMode
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
from freqtrade.exchange import Exchange
from freqtrade.exchange.common import retrier
from freqtrade.exchange.types import Tickers
from freqtrade.exchange.types import OHLCVResponse, Tickers
from freqtrade.misc import deep_merge_dicts, json_load
@@ -112,7 +112,7 @@ class Binance(Exchange):
since_ms: int, candle_type: CandleType,
is_new_pair: bool = False, raise_: bool = False,
until_ms: Optional[int] = None
) -> Tuple[str, str, str, List]:
) -> OHLCVResponse:
"""
Overwrite to introduce "fast new pair" functionality by detecting the pair's listing date
Does not work for other exchanges, which don't return the earliest data when called with "0"

View File

@@ -36,7 +36,7 @@ from freqtrade.exchange.exchange_utils import (CcxtModuleType, amount_to_contrac
price_to_precision, timeframe_to_minutes,
timeframe_to_msecs, timeframe_to_next_date,
timeframe_to_prev_date, timeframe_to_seconds)
from freqtrade.exchange.types import Ticker, Tickers
from freqtrade.exchange.types import OHLCVResponse, Ticker, Tickers
from freqtrade.misc import (chunks, deep_merge_dicts, file_dump_json, file_load_json,
safe_value_fallback2)
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
@@ -1813,32 +1813,18 @@ class Exchange:
:param candle_type: '', mark, index, premiumIndex, or funding_rate
:return: List with candle (OHLCV) data
"""
pair, _, _, data = self.loop.run_until_complete(
pair, _, _, data, _ = self.loop.run_until_complete(
self._async_get_historic_ohlcv(pair=pair, timeframe=timeframe,
since_ms=since_ms, until_ms=until_ms,
is_new_pair=is_new_pair, candle_type=candle_type))
logger.info(f"Downloaded data for {pair} with length {len(data)}.")
return data
def get_historic_ohlcv_as_df(self, pair: str, timeframe: str,
since_ms: int, candle_type: CandleType) -> DataFrame:
"""
Minimal wrapper around get_historic_ohlcv - converting the result into a dataframe
:param pair: Pair to download
:param timeframe: Timeframe to get data for
:param since_ms: Timestamp in milliseconds to get history from
:param candle_type: Any of the enum CandleType (must match trading mode!)
:return: OHLCV DataFrame
"""
ticks = self.get_historic_ohlcv(pair, timeframe, since_ms=since_ms, candle_type=candle_type)
return ohlcv_to_dataframe(ticks, timeframe, pair=pair, fill_missing=True,
drop_incomplete=self._ohlcv_partial_candle)
async def _async_get_historic_ohlcv(self, pair: str, timeframe: str,
since_ms: int, candle_type: CandleType,
is_new_pair: bool = False, raise_: bool = False,
until_ms: Optional[int] = None
) -> Tuple[str, str, str, List]:
) -> OHLCVResponse:
"""
Download historic ohlcv
:param is_new_pair: used by binance subclass to allow "fast" new pair downloading
@@ -1869,15 +1855,16 @@ class Exchange:
continue
else:
# Deconstruct tuple if it's not an exception
p, _, c, new_data = res
p, _, c, new_data, _ = res
if p == pair and c == candle_type:
data.extend(new_data)
# Sort data again after extending the result - above calls return in "async order"
data = sorted(data, key=lambda x: x[0])
return pair, timeframe, candle_type, data
return pair, timeframe, candle_type, data, self._ohlcv_partial_candle
def _build_coroutine(self, pair: str, timeframe: str, candle_type: CandleType,
since_ms: Optional[int], cache: bool) -> Coroutine:
def _build_coroutine(
self, pair: str, timeframe: str, candle_type: CandleType,
since_ms: Optional[int], cache: bool) -> Coroutine[Any, Any, OHLCVResponse]:
not_all_data = cache and self.required_candle_call_count > 1
if cache and (pair, timeframe, candle_type) in self._klines:
candle_limit = self.ohlcv_candle_limit(timeframe, candle_type)
@@ -1914,7 +1901,7 @@ class Exchange:
"""
Build Coroutines to execute as part of refresh_latest_ohlcv
"""
input_coroutines = []
input_coroutines: List[Coroutine[Any, Any, OHLCVResponse]] = []
cached_pairs = []
for pair, timeframe, candle_type in set(pair_list):
if (timeframe not in self.timeframes
@@ -1978,7 +1965,6 @@ class Exchange:
:return: Dict of [{(pair, timeframe): Dataframe}]
"""
logger.debug("Refreshing candle (OHLCV) data for %d pairs", len(pair_list))
drop_incomplete = self._ohlcv_partial_candle if drop_incomplete is None else drop_incomplete
# Gather coroutines to run
input_coroutines, cached_pairs = self._build_ohlcv_dl_jobs(pair_list, since_ms, cache)
@@ -1996,8 +1982,9 @@ class Exchange:
if isinstance(res, Exception):
logger.warning(f"Async code raised an exception: {repr(res)}")
continue
# Deconstruct tuple (has 4 elements)
pair, timeframe, c_type, ticks = res
# Deconstruct tuple (has 5 elements)
pair, timeframe, c_type, ticks, drop_hint = res
drop_incomplete = drop_hint if drop_incomplete is None else drop_incomplete
ohlcv_df = self._process_ohlcv_df(
pair, timeframe, c_type, ticks, cache, drop_incomplete)
@@ -2025,7 +2012,7 @@ class Exchange:
timeframe: str,
candle_type: CandleType,
since_ms: Optional[int] = None,
) -> Tuple[str, str, str, List]:
) -> OHLCVResponse:
"""
Asynchronously get candle history data using fetch_ohlcv
:param candle_type: '', mark, index, premiumIndex, or funding_rate
@@ -2065,9 +2052,9 @@ class Exchange:
data = sorted(data, key=lambda x: x[0])
except IndexError:
logger.exception("Error loading %s. Result was %s.", pair, data)
return pair, timeframe, candle_type, []
return pair, timeframe, candle_type, [], self._ohlcv_partial_candle
logger.debug("Done fetching pair %s, interval %s ...", pair, timeframe)
return pair, timeframe, candle_type, data
return pair, timeframe, candle_type, data, self._ohlcv_partial_candle
except ccxt.NotSupported as e:
raise OperationalException(
@@ -2764,11 +2751,16 @@ class Exchange:
"""
Important: Must be fetching data from cached values as this is used by backtesting!
PERPETUAL:
gateio: https://www.gate.io/help/futures/perpetual/22160/calculation-of-liquidation-price
gateio: https://www.gate.io/help/futures/futures/27724/liquidation-price-bankruptcy-price
> Liquidation Price = (Entry Price ± Margin / Contract Multiplier / Size) /
[ 1 ± (Maintenance Margin Ratio + Taker Rate)]
Wherein, "+" or "-" depends on whether the contract goes long or short:
"-" for long, and "+" for short.
okex: https://www.okex.com/support/hc/en-us/articles/
360053909592-VI-Introduction-to-the-isolated-mode-of-Single-Multi-currency-Portfolio-margin
:param exchange_name:
:param pair: Pair to calculate liquidation price for
:param open_rate: Entry price of position
:param is_short: True if the trade is a short, false otherwise
:param amount: Absolute value of position size incl. leverage (in base currency)
@@ -2808,7 +2800,7 @@ class Exchange:
def get_maintenance_ratio_and_amt(
self,
pair: str,
nominal_value: float = 0.0,
nominal_value: float,
) -> Tuple[float, Optional[float]]:
"""
Important: Must be fetching data from cached values as this is used by backtesting!

View File

@@ -1,4 +1,6 @@
from typing import Dict, Optional, TypedDict
from typing import Dict, List, Optional, Tuple, TypedDict
from freqtrade.enums import CandleType
class Ticker(TypedDict):
@@ -14,3 +16,6 @@ class Ticker(TypedDict):
Tickers = Dict[str, Ticker]
# pair, timeframe, candleType, OHLCV, drop last?,
OHLCVResponse = Tuple[str, str, CandleType, List, bool]

View File

@@ -1177,6 +1177,7 @@ class Backtesting:
open_trade_count_start = self.backtest_loop(
row, pair, current_time, end_date, max_open_trades,
open_trade_count_start)
continue
detail_data.loc[:, 'enter_long'] = row[LONG_IDX]
detail_data.loc[:, 'exit_long'] = row[ELONG_IDX]
detail_data.loc[:, 'enter_short'] = row[SHORT_IDX]

View File

@@ -28,7 +28,7 @@ class FreqaiExampleStrategy(IStrategy):
plot_config = {
"main_plot": {},
"subplots": {
"prediction": {"prediction": {"color": "blue"}},
"&-s_close": {"prediction": {"color": "blue"}},
"do_predict": {
"do_predict": {"color": "brown"},
},
@@ -140,7 +140,8 @@ class FreqaiExampleStrategy(IStrategy):
# If user wishes to use multiple targets, they can add more by
# appending more columns with '&'. User should keep in mind that multi targets
# requires a multioutput prediction model such as
# templates/CatboostPredictionMultiModel.py,
# freqai/prediction_models/CatboostRegressorMultiTarget.py,
# freqtrade trade --freqaimodel CatboostRegressorMultiTarget
# df["&-s_range"] = (
# df["close"]