merge dev into backtest-live-predictions

This commit is contained in:
robcaulk 2022-11-17 21:20:47 +01:00
commit 91df79ff44
13 changed files with 186 additions and 239 deletions

View File

@ -3,11 +3,12 @@ This module contains the argument manager class
"""
import logging
import re
from datetime import datetime
from datetime import datetime, timezone
from typing import Optional
import arrow
from freqtrade.constants import DATETIME_PRINT_FORMAT
from freqtrade.exceptions import OperationalException
@ -29,6 +30,52 @@ class TimeRange:
self.startts: int = startts
self.stopts: int = stopts
@property
def startdt(self) -> Optional[datetime]:
if self.startts:
return datetime.fromtimestamp(self.startts, tz=timezone.utc)
return None
@property
def stopdt(self) -> Optional[datetime]:
if self.stopts:
return datetime.fromtimestamp(self.stopts, tz=timezone.utc)
return None
@property
def timerange_str(self) -> str:
"""
Returns a string representation of the timerange as used by parse_timerange.
Follows the format yyyymmdd-yyyymmdd - leaving out the parts that are not set.
"""
start = ''
stop = ''
if startdt := self.startdt:
start = startdt.strftime('%Y%m%d')
if stopdt := self.stopdt:
stop = stopdt.strftime('%Y%m%d')
return f"{start}-{stop}"
@property
def start_fmt(self) -> str:
"""
Returns a string representation of the start date
"""
val = 'unbounded'
if (startdt := self.startdt) is not None:
val = startdt.strftime(DATETIME_PRINT_FORMAT)
return val
@property
def stop_fmt(self) -> str:
"""
Returns a string representation of the stop date
"""
val = 'unbounded'
if (stopdt := self.stopdt) is not None:
val = stopdt.strftime(DATETIME_PRINT_FORMAT)
return val
def __eq__(self, other):
"""Override the default Equals behavior"""
return (self.starttype == other.starttype and self.stoptype == other.stoptype

View File

@ -3,7 +3,6 @@ Functions to convert data from one format to another
"""
import itertools
import logging
from datetime import datetime, timezone
from operator import itemgetter
from typing import Dict, List
@ -138,11 +137,9 @@ def trim_dataframe(df: DataFrame, timerange, df_date_col: str = 'date',
df = df.iloc[startup_candles:, :]
else:
if timerange.starttype == 'date':
start = datetime.fromtimestamp(timerange.startts, tz=timezone.utc)
df = df.loc[df[df_date_col] >= start, :]
df = df.loc[df[df_date_col] >= timerange.startdt, :]
if timerange.stoptype == 'date':
stop = datetime.fromtimestamp(timerange.stopts, tz=timezone.utc)
df = df.loc[df[df_date_col] <= stop, :]
df = df.loc[df[df_date_col] <= timerange.stopdt, :]
return df

View File

@ -1,6 +1,6 @@
import logging
import operator
from datetime import datetime, timezone
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional, Tuple
@ -160,9 +160,9 @@ def _load_cached_data_for_updating(
end = None
if timerange:
if timerange.starttype == 'date':
start = datetime.fromtimestamp(timerange.startts, tz=timezone.utc)
start = timerange.startdt
if timerange.stoptype == 'date':
end = datetime.fromtimestamp(timerange.stopts, tz=timezone.utc)
end = timerange.stopdt
# Intentionally don't pass timerange in - since we need to load the full dataset.
data = data_handler.ohlcv_load(pair, timeframe=timeframe,

View File

@ -366,13 +366,11 @@ class IDataHandler(ABC):
"""
if timerange.starttype == 'date':
start = datetime.fromtimestamp(timerange.startts, tz=timezone.utc)
if pairdata.iloc[0]['date'] > start:
if pairdata.iloc[0]['date'] > timerange.startdt:
logger.warning(f"{pair}, {candle_type}, {timeframe}, "
f"data starts at {pairdata.iloc[0]['date']:%Y-%m-%d %H:%M:%S}")
if timerange.stoptype == 'date':
stop = datetime.fromtimestamp(timerange.stopts, tz=timezone.utc)
if pairdata.iloc[-1]['date'] < stop:
if pairdata.iloc[-1]['date'] < timerange.stopdt:
logger.warning(f"{pair}, {candle_type}, {timeframe}, "
f"data ends at {pairdata.iloc[-1]['date']:%Y-%m-%d %H:%M:%S}")

View File

@ -436,9 +436,7 @@ class FreqaiDataKitchen:
timerange_train.stopts = timerange_train.startts + train_period_days
first = False
start = datetime.fromtimestamp(timerange_train.startts, tz=timezone.utc)
stop = datetime.fromtimestamp(timerange_train.stopts, tz=timezone.utc)
tr_training_list.append(start.strftime("%Y%m%d") + "-" + stop.strftime("%Y%m%d"))
tr_training_list.append(timerange_train.timerange_str)
tr_training_list_timerange.append(copy.deepcopy(timerange_train))
# associated backtest period
@ -450,9 +448,7 @@ class FreqaiDataKitchen:
if timerange_backtest.stopts > config_timerange.stopts:
timerange_backtest.stopts = config_timerange.stopts
start = datetime.fromtimestamp(timerange_backtest.startts, tz=timezone.utc)
stop = datetime.fromtimestamp(timerange_backtest.stopts, tz=timezone.utc)
tr_backtesting_list.append(start.strftime("%Y%m%d") + "-" + stop.strftime("%Y%m%d"))
tr_backtesting_list.append(timerange_backtest.timerange_str)
tr_backtesting_list_timerange.append(copy.deepcopy(timerange_backtest))
# ensure we are predicting on exactly same amount of data as requested by user defined
@ -494,11 +490,9 @@ class FreqaiDataKitchen:
it is sliced down to just the present training period.
"""
start = datetime.fromtimestamp(timerange.startts, tz=timezone.utc)
stop = datetime.fromtimestamp(timerange.stopts, tz=timezone.utc)
df = df.loc[df["date"] >= start, :]
df = df.loc[df["date"] >= timerange.startdt, :]
if not self.live:
df = df.loc[df["date"] < stop, :]
df = df.loc[df["date"] < timerange.stopdt, :]
return df
@ -1059,9 +1053,7 @@ class FreqaiDataKitchen:
backtest_timerange.startts = (
backtest_timerange.startts - backtest_period_days * SECONDS_IN_DAY
)
start = datetime.fromtimestamp(backtest_timerange.startts, tz=timezone.utc)
stop = datetime.fromtimestamp(backtest_timerange.stopts, tz=timezone.utc)
full_timerange = start.strftime("%Y%m%d") + "-" + stop.strftime("%Y%m%d")
full_timerange = backtest_timerange.timerange_str
config_path = Path(self.config["config_files"][0])
if not self.full_path.is_dir():

View File

@ -13,7 +13,7 @@ from numpy.typing import NDArray
from pandas import DataFrame
from freqtrade.configuration import TimeRange
from freqtrade.constants import DATETIME_PRINT_FORMAT, Config
from freqtrade.constants import Config
from freqtrade.enums import RunMode
from freqtrade.exceptions import OperationalException
from freqtrade.exchange import timeframe_to_seconds
@ -826,14 +826,8 @@ class IFreqaiModel(ABC):
:return: if the data exists or not
"""
if self.config.get("freqai_backtest_live_models", False) and len(dataframe_backtest) == 0:
tr_backtest_startts_str = datetime.fromtimestamp(
tr_backtest.startts,
tz=timezone.utc).strftime(DATETIME_PRINT_FORMAT)
tr_backtest_stopts_str = datetime.fromtimestamp(
tr_backtest.stopts,
tz=timezone.utc).strftime(DATETIME_PRINT_FORMAT)
logger.info(f"No data found for pair {pair} from {tr_backtest_startts_str} "
f" from {tr_backtest_startts_str} to {tr_backtest_stopts_str}. "
logger.info(f"No data found for pair {pair} from "
f"from { tr_backtest.start_fmt} to {tr_backtest.stop_fmt}. "
"Probably more than one training within the same candle period.")
return False
return True
@ -848,18 +842,11 @@ class IFreqaiModel(ABC):
:param pair: the current pair
:param total_trains: total trains (total number of slides for the sliding window)
"""
tr_train_startts_str = datetime.fromtimestamp(
tr_train.startts,
tz=timezone.utc).strftime(DATETIME_PRINT_FORMAT)
tr_train_stopts_str = datetime.fromtimestamp(
tr_train.stopts,
tz=timezone.utc).strftime(DATETIME_PRINT_FORMAT)
if not self.config.get("freqai_backtest_live_models", False):
logger.info(
f"Training {pair}, {self.pair_it}/{self.total_pairs} pairs"
f" from {tr_train_startts_str} "
f"to {tr_train_stopts_str}, {train_it}/{total_trains} "
f" from {tr_train.start_fmt} "
f"to {tr_train.stop_fmt}, {train_it}/{total_trains} "
"trains"
)

View File

@ -235,7 +235,4 @@ def get_timerange_backtest_live_models(config: Config) -> str:
else:
timerange = dk.get_timerange_from_backtesting_live_dataframe()
start_date = datetime.fromtimestamp(timerange.startts, tz=timezone.utc)
end_date = datetime.fromtimestamp(timerange.stopts, tz=timezone.utc)
tr = f"{start_date.strftime('%Y%m%d')}-{end_date.strftime('%Y%m%d')}"
return tr
return timerange.timerange_str

View File

@ -1286,8 +1286,7 @@ class Backtesting:
def _get_min_cached_backtest_date(self):
min_backtest_date = None
backtest_cache_age = self.config.get('backtest_cache', constants.BACKTEST_CACHE_DEFAULT)
if self.timerange.stopts == 0 or datetime.fromtimestamp(
self.timerange.stopts, tz=timezone.utc) > datetime.now(tz=timezone.utc):
if self.timerange.stopts == 0 or self.timerange.stopdt > datetime.now(tz=timezone.utc):
logger.warning('Backtest result caching disabled due to use of open-ended timerange.')
elif backtest_cache_age == 'day':
min_backtest_date = datetime.now(tz=timezone.utc) - timedelta(days=1)

View File

@ -90,6 +90,13 @@ class Order(_DECL_BASE):
def safe_filled(self) -> float:
return self.filled if self.filled is not None else self.amount or 0.0
@property
def safe_remaining(self) -> float:
return (
self.remaining if self.remaining is not None else
self.amount - (self.filled or 0.0)
)
@property
def safe_fee_base(self) -> float:
return self.ft_fee_base or 0.0

View File

@ -218,9 +218,10 @@ class RPC:
stoploss_current_dist_pct=round(stoploss_current_dist_ratio * 100, 2),
stoploss_entry_dist=stoploss_entry_dist,
stoploss_entry_dist_ratio=round(stoploss_entry_dist_ratio, 8),
open_order='({} {} rem={:.8f})'.format(
order.order_type, order.side, order.remaining
) if order else None,
open_order=(
f'({order.order_type} {order.side} rem={order.safe_remaining:.8f})' if
order else None
),
))
results.append(trade_dict)
return results

View File

@ -1062,7 +1062,7 @@ class Telegram(RPCHandler):
self._rpc._rpc_force_entry(pair, price, order_side=order_side)
except RPCException as e:
logger.exception("Forcebuy error!")
self._send_msg(str(e))
self._send_msg(str(e), ParseMode.HTML)
def _force_enter_inline(self, update: Update, _: CallbackContext) -> None:
if update.callback_query:

View File

@ -1,6 +1,7 @@
# pragma pylint: disable=missing-docstring, C0103
# pragma pylint: disable=invalid-sequence-index, invalid-name, too-many-arguments
from copy import deepcopy
from datetime import datetime, timedelta, timezone
from unittest.mock import ANY, MagicMock, PropertyMock
@ -28,113 +29,7 @@ def prec_satoshi(a, b) -> float:
# Unit tests
def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=ticker,
get_fee=fee,
_is_dry_limit_order_filled=MagicMock(side_effect=[False, True]),
)
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
patch_get_signal(freqtradebot)
rpc = RPC(freqtradebot)
freqtradebot.state = State.RUNNING
with pytest.raises(RPCException, match=r'.*no active trade*'):
rpc._rpc_trade_status()
freqtradebot.enter_positions()
# Open order...
results = rpc._rpc_trade_status()
assert results[0] == {
'trade_id': 1,
'pair': 'ETH/BTC',
'base_currency': 'ETH',
'quote_currency': 'BTC',
'open_date': ANY,
'open_timestamp': ANY,
'is_open': ANY,
'fee_open': ANY,
'fee_open_cost': ANY,
'fee_open_currency': ANY,
'fee_close': fee.return_value,
'fee_close_cost': ANY,
'fee_close_currency': ANY,
'open_rate_requested': ANY,
'open_trade_value': 0.0010025,
'close_rate_requested': ANY,
'sell_reason': ANY,
'exit_reason': ANY,
'exit_order_status': ANY,
'min_rate': ANY,
'max_rate': ANY,
'strategy': ANY,
'buy_tag': ANY,
'enter_tag': ANY,
'timeframe': 5,
'open_order_id': ANY,
'close_date': None,
'close_timestamp': None,
'open_rate': 1.098e-05,
'close_rate': None,
'current_rate': 1.099e-05,
'amount': 91.07468124,
'amount_requested': 91.07468124,
'stake_amount': 0.001,
'trade_duration': None,
'trade_duration_s': None,
'close_profit': None,
'close_profit_pct': None,
'close_profit_abs': None,
'current_profit': 0.0,
'current_profit_pct': 0.0,
'current_profit_abs': 0.0,
'profit_ratio': 0.0,
'profit_pct': 0.0,
'profit_abs': 0.0,
'profit_fiat': ANY,
'stop_loss_abs': 0.0,
'stop_loss_pct': None,
'stop_loss_ratio': None,
'stoploss_order_id': None,
'stoploss_last_update': ANY,
'stoploss_last_update_timestamp': ANY,
'initial_stop_loss_abs': 0.0,
'initial_stop_loss_pct': None,
'initial_stop_loss_ratio': None,
'stoploss_current_dist': -1.099e-05,
'stoploss_current_dist_ratio': -1.0,
'stoploss_current_dist_pct': pytest.approx(-100.0),
'stoploss_entry_dist': -0.0010025,
'stoploss_entry_dist_ratio': -1.0,
'open_order': '(limit buy rem=91.07468123)',
'realized_profit': 0.0,
'exchange': 'binance',
'leverage': 1.0,
'interest_rate': 0.0,
'liquidation_price': None,
'is_short': False,
'funding_fees': 0.0,
'trading_mode': TradingMode.SPOT,
'orders': [{
'amount': 91.07468123, 'average': 1.098e-05, 'safe_price': 1.098e-05,
'cost': 0.0009999999999054, 'filled': 0.0, 'ft_order_side': 'buy',
'order_date': ANY, 'order_timestamp': ANY, 'order_filled_date': ANY,
'order_filled_timestamp': ANY, 'order_type': 'limit', 'price': 1.098e-05,
'is_open': True, 'pair': 'ETH/BTC', 'order_id': ANY,
'remaining': 91.07468123, 'status': ANY, 'ft_is_entry': True,
}],
}
# Fill open order ...
freqtradebot.manage_open_orders()
trades = Trade.get_open_trades()
freqtradebot.exit_positions(trades)
results = rpc._rpc_trade_status()
assert results[0] == {
gen_response = {
'trade_id': 1,
'pair': 'ETH/BTC',
'base_currency': 'ETH',
@ -213,91 +108,103 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'remaining': ANY, 'status': ANY, 'ft_is_entry': True,
}],
}
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=ticker,
get_fee=fee,
_is_dry_limit_order_filled=MagicMock(side_effect=[False, True]),
)
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
patch_get_signal(freqtradebot)
rpc = RPC(freqtradebot)
freqtradebot.state = State.RUNNING
with pytest.raises(RPCException, match=r'.*no active trade*'):
rpc._rpc_trade_status()
freqtradebot.enter_positions()
# Open order...
results = rpc._rpc_trade_status()
response_unfilled = deepcopy(gen_response)
# Different from "filled" response:
response_unfilled.update({
'amount': 91.07468124,
'profit_ratio': 0.0,
'profit_pct': 0.0,
'profit_abs': 0.0,
'current_profit': 0.0,
'current_profit_pct': 0.0,
'current_profit_abs': 0.0,
'stop_loss_abs': 0.0,
'stop_loss_pct': None,
'stop_loss_ratio': None,
'stoploss_current_dist': -1.099e-05,
'stoploss_current_dist_ratio': -1.0,
'stoploss_current_dist_pct': pytest.approx(-100.0),
'stoploss_entry_dist': -0.0010025,
'stoploss_entry_dist_ratio': -1.0,
'initial_stop_loss_abs': 0.0,
'initial_stop_loss_pct': None,
'initial_stop_loss_ratio': None,
'open_order': '(limit buy rem=91.07468123)',
})
response_unfilled['orders'][0].update({
'is_open': True,
'filled': 0.0,
'remaining': 91.07468123
})
assert results[0] == response_unfilled
# Open order without remaining
trade = Trade.get_open_trades()[0]
# kucoin case (no remaining set).
trade.orders[0].remaining = None
Trade.commit()
results = rpc._rpc_trade_status()
# Reuse above object, only remaining changed.
response_unfilled['orders'][0].update({
'remaining': None
})
assert results[0] == response_unfilled
trade = Trade.get_open_trades()[0]
trade.orders[0].remaining = trade.amount
Trade.commit()
# Fill open order ...
freqtradebot.manage_open_orders()
trades = Trade.get_open_trades()
freqtradebot.exit_positions(trades)
results = rpc._rpc_trade_status()
response = deepcopy(gen_response)
assert results[0] == response
mocker.patch('freqtrade.exchange.Exchange.get_rate',
MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available")))
results = rpc._rpc_trade_status()
assert isnan(results[0]['current_profit'])
assert isnan(results[0]['current_rate'])
assert results[0] == {
'trade_id': 1,
'pair': 'ETH/BTC',
'base_currency': 'ETH',
'quote_currency': 'BTC',
'open_date': ANY,
'open_timestamp': ANY,
'is_open': ANY,
'fee_open': ANY,
'fee_open_cost': ANY,
'fee_open_currency': ANY,
'fee_close': fee.return_value,
'fee_close_cost': ANY,
'fee_close_currency': ANY,
'open_rate_requested': ANY,
'open_trade_value': ANY,
'close_rate_requested': ANY,
'sell_reason': ANY,
'exit_reason': ANY,
'exit_order_status': ANY,
'min_rate': ANY,
'max_rate': ANY,
'strategy': ANY,
'buy_tag': ANY,
'enter_tag': ANY,
'timeframe': ANY,
'open_order_id': ANY,
'close_date': None,
'close_timestamp': None,
'open_rate': 1.098e-05,
'close_rate': None,
'current_rate': ANY,
'amount': 91.07468123,
'amount_requested': 91.07468124,
'trade_duration': ANY,
'trade_duration_s': ANY,
'stake_amount': 0.001,
'close_profit': None,
'close_profit_pct': None,
'close_profit_abs': None,
'current_profit': ANY,
'current_profit_pct': ANY,
'current_profit_abs': ANY,
'profit_ratio': ANY,
'profit_pct': ANY,
'profit_abs': ANY,
'profit_fiat': ANY,
'stop_loss_abs': 9.89e-06,
'stop_loss_pct': -10.0,
'stop_loss_ratio': -0.1,
'stoploss_order_id': None,
'stoploss_last_update': ANY,
'stoploss_last_update_timestamp': ANY,
'initial_stop_loss_abs': 9.89e-06,
'initial_stop_loss_pct': -10.0,
'initial_stop_loss_ratio': -0.1,
response_norate = deepcopy(gen_response)
# Update elements that are NaN when no rate is available.
response_norate.update({
'stoploss_current_dist': ANY,
'stoploss_current_dist_ratio': ANY,
'stoploss_current_dist_pct': ANY,
'stoploss_entry_dist': -0.00010402,
'stoploss_entry_dist_ratio': -0.10376381,
'open_order': None,
'exchange': 'binance',
'realized_profit': 0.0,
'leverage': 1.0,
'interest_rate': 0.0,
'liquidation_price': None,
'is_short': False,
'funding_fees': 0.0,
'trading_mode': TradingMode.SPOT,
'orders': [{
'amount': 91.07468123, 'average': 1.098e-05, 'safe_price': 1.098e-05,
'cost': 0.0009999999999054, 'filled': 91.07468123, 'ft_order_side': 'buy',
'order_date': ANY, 'order_timestamp': ANY, 'order_filled_date': ANY,
'order_filled_timestamp': ANY, 'order_type': 'limit', 'price': 1.098e-05,
'is_open': False, 'pair': 'ETH/BTC', 'order_id': ANY,
'remaining': ANY, 'status': ANY, 'ft_is_entry': True,
}],
}
'profit_ratio': ANY,
'profit_pct': ANY,
'profit_abs': ANY,
'current_profit_abs': ANY,
'current_profit': ANY,
'current_profit_pct': ANY,
'current_rate': ANY,
})
assert results[0] == response_norate
def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:

View File

@ -1,4 +1,6 @@
# pragma pylint: disable=missing-docstring, C0103
from datetime import datetime, timezone
import arrow
import pytest
@ -8,16 +10,28 @@ from freqtrade.exceptions import OperationalException
def test_parse_timerange_incorrect():
assert TimeRange('date', None, 1274486400, 0) == TimeRange.parse_timerange('20100522-')
assert TimeRange(None, 'date', 0, 1274486400) == TimeRange.parse_timerange('-20100522')
timerange = TimeRange.parse_timerange('20100522-')
assert TimeRange('date', None, 1274486400, 0) == timerange
assert timerange.timerange_str == '20100522-'
timerange = TimeRange.parse_timerange('-20100522')
assert TimeRange(None, 'date', 0, 1274486400) == timerange
assert timerange.timerange_str == '-20100522'
timerange = TimeRange.parse_timerange('20100522-20150730')
assert timerange == TimeRange('date', 'date', 1274486400, 1438214400)
assert timerange.timerange_str == '20100522-20150730'
assert timerange.start_fmt == '2010-05-22 00:00:00'
assert timerange.stop_fmt == '2015-07-30 00:00:00'
# Added test for unix timestamp - BTC genesis date
assert TimeRange('date', None, 1231006505, 0) == TimeRange.parse_timerange('1231006505-')
assert TimeRange(None, 'date', 0, 1233360000) == TimeRange.parse_timerange('-1233360000')
timerange = TimeRange.parse_timerange('1231006505-1233360000')
assert TimeRange('date', 'date', 1231006505, 1233360000) == timerange
assert isinstance(timerange.startdt, datetime)
assert isinstance(timerange.stopdt, datetime)
assert timerange.startdt == datetime.fromtimestamp(1231006505, tz=timezone.utc)
assert timerange.stopdt == datetime.fromtimestamp(1233360000, tz=timezone.utc)
assert timerange.timerange_str == '20090103-20090131'
timerange = TimeRange.parse_timerange('1231006505000-1233360000000')
assert TimeRange('date', 'date', 1231006505, 1233360000) == timerange
@ -45,6 +59,7 @@ def test_subtract_start():
x = TimeRange(None, 'date', 0, 1438214400)
x.subtract_start(300)
assert not x.startts
assert not x.startdt
x = TimeRange('date', None, 1274486400, 0)
x.subtract_start(300)