merge develop into RL
This commit is contained in:
@@ -108,7 +108,6 @@ def ask_user_config() -> Dict[str, Any]:
|
||||
"binance",
|
||||
"binanceus",
|
||||
"bittrex",
|
||||
"ftx",
|
||||
"gateio",
|
||||
"huobi",
|
||||
"kraken",
|
||||
|
@@ -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
|
||||
|
@@ -159,6 +159,7 @@ CONF_SCHEMA = {
|
||||
'ignore_buying_expired_candle_after': {'type': 'number'},
|
||||
'trading_mode': {'type': 'string', 'enum': TRADING_MODES},
|
||||
'margin_mode': {'type': 'string', 'enum': MARGIN_MODES},
|
||||
'reduce_df_footprint': {'type': 'boolean', 'default': False},
|
||||
'liquidation_buffer': {'type': 'number', 'minimum': 0.0, 'maximum': 0.99},
|
||||
'backtest_breakdown': {
|
||||
'type': 'array',
|
||||
|
@@ -3,10 +3,10 @@ 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
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from pandas import DataFrame, to_datetime
|
||||
|
||||
@@ -137,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
|
||||
|
||||
|
||||
@@ -313,3 +311,29 @@ def convert_ohlcv_format(
|
||||
if erase and convert_from != convert_to:
|
||||
logger.info(f"Deleting source data for {pair} / {timeframe}")
|
||||
src.ohlcv_purge(pair=pair, timeframe=timeframe, candle_type=candle_type)
|
||||
|
||||
|
||||
def reduce_dataframe_footprint(df: DataFrame) -> DataFrame:
|
||||
"""
|
||||
Ensure all values are float32 in the incoming dataframe.
|
||||
:param df: Dataframe to be converted to float/int 32s
|
||||
:return: Dataframe converted to float/int 32s
|
||||
"""
|
||||
|
||||
logger.debug(f"Memory usage of dataframe is "
|
||||
f"{df.memory_usage().sum() / 1024**2:.2f} MB")
|
||||
|
||||
df_dtypes = df.dtypes
|
||||
for column, dtype in df_dtypes.items():
|
||||
if column in ['open', 'high', 'low', 'close', 'volume']:
|
||||
continue
|
||||
if dtype == np.float64:
|
||||
df_dtypes[column] = np.float32
|
||||
elif dtype == np.int64:
|
||||
df_dtypes[column] = np.int32
|
||||
df = df.astype(df_dtypes)
|
||||
|
||||
logger.debug(f"Memory usage after optimization is: "
|
||||
f"{df.memory_usage().sum() / 1024**2:.2f} MB")
|
||||
|
||||
return df
|
||||
|
@@ -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,
|
||||
|
@@ -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}")
|
||||
|
||||
|
@@ -392,7 +392,7 @@ class Edge:
|
||||
# Returning a list of pairs in order of "expectancy"
|
||||
return final
|
||||
|
||||
def _find_trades_for_stoploss_range(self, df, pair, stoploss_range):
|
||||
def _find_trades_for_stoploss_range(self, df, pair: str, stoploss_range) -> list:
|
||||
buy_column = df['enter_long'].values
|
||||
sell_column = df['exit_long'].values
|
||||
date_column = df['date'].values
|
||||
@@ -407,7 +407,7 @@ class Edge:
|
||||
return result
|
||||
|
||||
def _detect_next_stop_or_sell_point(self, buy_column, sell_column, date_column,
|
||||
ohlc_columns, stoploss, pair):
|
||||
ohlc_columns, stoploss, pair: str):
|
||||
"""
|
||||
Iterate through ohlc_columns in order to find the next trade
|
||||
Next trade opens from the first buy signal noticed to
|
||||
|
@@ -18,7 +18,6 @@ from freqtrade.exchange.exchange_utils import (amount_to_contract_precision, amo
|
||||
timeframe_to_next_date, timeframe_to_prev_date,
|
||||
timeframe_to_seconds, validate_exchange,
|
||||
validate_exchanges)
|
||||
from freqtrade.exchange.ftx import Ftx
|
||||
from freqtrade.exchange.gateio import Gateio
|
||||
from freqtrade.exchange.hitbtc import Hitbtc
|
||||
from freqtrade.exchange.huobi import Huobi
|
||||
|
@@ -52,7 +52,6 @@ MAP_EXCHANGE_CHILDCLASS = {
|
||||
SUPPORTED_EXCHANGES = [
|
||||
'binance',
|
||||
'bittrex',
|
||||
'ftx',
|
||||
'gateio',
|
||||
'huobi',
|
||||
'kraken',
|
||||
|
@@ -1,178 +0,0 @@
|
||||
""" FTX exchange subclass """
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import ccxt
|
||||
|
||||
from freqtrade.constants import BuySell
|
||||
from freqtrade.enums import MarginMode, TradingMode
|
||||
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
|
||||
OperationalException, TemporaryError)
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.common import API_FETCH_ORDER_RETRY_COUNT, retrier
|
||||
from freqtrade.misc import safe_value_fallback2
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Ftx(Exchange):
|
||||
|
||||
_ft_has: Dict = {
|
||||
"order_time_in_force": ['GTC', 'IOC', 'PO'],
|
||||
"stoploss_on_exchange": True,
|
||||
"ohlcv_candle_limit": 1500,
|
||||
"ohlcv_require_since": True,
|
||||
"ohlcv_volume_currency": "quote",
|
||||
"mark_ohlcv_price": "index",
|
||||
"mark_ohlcv_timeframe": "1h",
|
||||
}
|
||||
|
||||
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
|
||||
# TradingMode.SPOT always supported and not required in this list
|
||||
# (TradingMode.MARGIN, MarginMode.CROSS),
|
||||
# (TradingMode.FUTURES, MarginMode.CROSS)
|
||||
]
|
||||
|
||||
def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool:
|
||||
"""
|
||||
Verify stop_loss against stoploss-order value (limit or price)
|
||||
Returns True if adjustment is necessary.
|
||||
"""
|
||||
return order['type'] == 'stop' and (
|
||||
side == "sell" and stop_loss > float(order['price']) or
|
||||
side == "buy" and stop_loss < float(order['price'])
|
||||
)
|
||||
|
||||
@retrier(retries=0)
|
||||
def stoploss(self, pair: str, amount: float, stop_price: float,
|
||||
order_types: Dict, side: BuySell, leverage: float) -> Dict:
|
||||
"""
|
||||
Creates a stoploss order.
|
||||
depending on order_types.stoploss configuration, uses 'market' or limit order.
|
||||
|
||||
Limit orders are defined by having orderPrice set, otherwise a market order is used.
|
||||
"""
|
||||
limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
|
||||
if side == "sell":
|
||||
limit_rate = stop_price * limit_price_pct
|
||||
else:
|
||||
limit_rate = stop_price * (2 - limit_price_pct)
|
||||
|
||||
ordertype = "stop"
|
||||
|
||||
stop_price = self.price_to_precision(pair, stop_price)
|
||||
|
||||
if self._config['dry_run']:
|
||||
dry_order = self.create_dry_run_order(
|
||||
pair, ordertype, side, amount, stop_price, leverage, stop_loss=True)
|
||||
return dry_order
|
||||
|
||||
try:
|
||||
params = self._params.copy()
|
||||
if order_types.get('stoploss', 'market') == 'limit':
|
||||
# set orderPrice to place limit order, otherwise it's a market order
|
||||
params['orderPrice'] = limit_rate
|
||||
if self.trading_mode == TradingMode.FUTURES:
|
||||
params.update({'reduceOnly': True})
|
||||
|
||||
params['stopPrice'] = stop_price
|
||||
amount = self.amount_to_precision(pair, amount)
|
||||
|
||||
self._lev_prep(pair, leverage, side)
|
||||
order = self._api.create_order(symbol=pair, type=ordertype, side=side,
|
||||
amount=amount, params=params)
|
||||
self._log_exchange_response('create_stoploss_order', order)
|
||||
logger.info('stoploss order added for %s. '
|
||||
'stop price: %s.', pair, stop_price)
|
||||
return order
|
||||
except ccxt.InsufficientFunds as e:
|
||||
raise InsufficientFundsError(
|
||||
f'Insufficient funds to create {ordertype} {side} order on market {pair}. '
|
||||
f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
|
||||
f'Message: {e}') from e
|
||||
except ccxt.InvalidOrder as e:
|
||||
raise InvalidOrderException(
|
||||
f'Could not create {ordertype} {side} order on market {pair}. '
|
||||
f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
|
||||
f'Message: {e}') from e
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not place {side} order due to {e.__class__.__name__}. Message: {e}') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
@retrier(retries=API_FETCH_ORDER_RETRY_COUNT)
|
||||
def fetch_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
|
||||
if self._config['dry_run']:
|
||||
return self.fetch_dry_run_order(order_id)
|
||||
|
||||
try:
|
||||
orders = self._api.fetch_orders(pair, None, params={'type': 'stop'})
|
||||
|
||||
order = [order for order in orders if order['id'] == order_id]
|
||||
self._log_exchange_response('fetch_stoploss_order', order)
|
||||
if len(order) == 1:
|
||||
if order[0].get('status') == 'closed':
|
||||
# Trigger order was triggered ...
|
||||
real_order_id: Optional[str] = order[0].get('info', {}).get('orderId')
|
||||
# OrderId may be None for stoploss-market orders
|
||||
# So we need to get it through the endpoint
|
||||
# /conditional_orders/{conditional_order_id}/triggers
|
||||
if not real_order_id:
|
||||
res = self._api.privateGetConditionalOrdersConditionalOrderIdTriggers(
|
||||
params={'conditional_order_id': order_id})
|
||||
self._log_exchange_response('fetch_stoploss_order2', res)
|
||||
real_order_id = res['result'][0]['orderId'] if res.get(
|
||||
'result', []) else None
|
||||
|
||||
if real_order_id:
|
||||
order1 = self._api.fetch_order(real_order_id, pair)
|
||||
self._log_exchange_response('fetch_stoploss_order1', order1)
|
||||
# Fake type to stop - as this was really a stop order.
|
||||
order1['id_stop'] = order1['id']
|
||||
order1['id'] = order_id
|
||||
order1['type'] = 'stop'
|
||||
order1['status_stop'] = 'triggered'
|
||||
return order1
|
||||
|
||||
return order[0]
|
||||
else:
|
||||
raise InvalidOrderException(f"Could not get stoploss order for id {order_id}")
|
||||
|
||||
except ccxt.InvalidOrder as e:
|
||||
raise InvalidOrderException(
|
||||
f'Tried to get an invalid order (id: {order_id}). Message: {e}') from e
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not get order due to {e.__class__.__name__}. Message: {e}') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
@retrier
|
||||
def cancel_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
|
||||
if self._config['dry_run']:
|
||||
return {}
|
||||
try:
|
||||
order = self._api.cancel_order(order_id, pair, params={'type': 'stop'})
|
||||
self._log_exchange_response('cancel_stoploss_order', order)
|
||||
return order
|
||||
except ccxt.InvalidOrder as e:
|
||||
raise InvalidOrderException(
|
||||
f'Could not cancel order. Message: {e}') from e
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not cancel order due to {e.__class__.__name__}. Message: {e}') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
def get_order_id_conditional(self, order: Dict[str, Any]) -> str:
|
||||
if order['type'] == 'stop':
|
||||
return safe_value_fallback2(order, order, 'id_stop', 'id')
|
||||
return order['id']
|
@@ -20,6 +20,7 @@ from sklearn.neighbors import NearestNeighbors
|
||||
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.constants import Config
|
||||
from freqtrade.data.converter import reduce_dataframe_footprint
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import timeframe_to_seconds
|
||||
from freqtrade.strategy.interface import IStrategy
|
||||
@@ -436,9 +437,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 +449,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 +491,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
|
||||
|
||||
@@ -1061,9 +1056,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():
|
||||
@@ -1279,6 +1272,9 @@ class FreqaiDataKitchen:
|
||||
|
||||
dataframe = self.remove_special_chars_from_feature_names(dataframe)
|
||||
|
||||
if self.config.get('reduce_df_footprint', False):
|
||||
dataframe = reduce_dataframe_footprint(dataframe)
|
||||
|
||||
return dataframe
|
||||
|
||||
def fit_labels(self) -> None:
|
||||
|
@@ -14,7 +14,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.data.dataprovider import DataProvider
|
||||
from freqtrade.enums import RunMode
|
||||
from freqtrade.exceptions import OperationalException
|
||||
@@ -803,14 +803,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
|
||||
@@ -825,18 +819,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"
|
||||
)
|
||||
# Following methods which are overridden by user made prediction models.
|
||||
|
@@ -230,7 +230,4 @@ def get_timerange_backtest_live_models(config: Config) -> str:
|
||||
dk = FreqaiDataKitchen(config)
|
||||
models_path = dk.get_full_models_path(config)
|
||||
timerange, _ = dk.get_timerange_and_assets_end_dates_from_ready_models(models_path)
|
||||
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
|
||||
|
@@ -354,7 +354,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
if self.trading_mode == TradingMode.FUTURES:
|
||||
self._schedule.run_pending()
|
||||
|
||||
def update_closed_trades_without_assigned_fees(self):
|
||||
def update_closed_trades_without_assigned_fees(self) -> None:
|
||||
"""
|
||||
Update closed trades without close fees assigned.
|
||||
Only acts when Orders are in the database, otherwise the last order-id is unknown.
|
||||
@@ -379,7 +379,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
stoploss_order=order.ft_order_side == 'stoploss',
|
||||
send_msg=False)
|
||||
|
||||
trades: List[Trade] = Trade.get_open_trades_without_assigned_fees()
|
||||
trades = Trade.get_open_trades_without_assigned_fees()
|
||||
for trade in trades:
|
||||
if trade.is_open and not trade.fee_updated(trade.entry_side):
|
||||
order = trade.select_order(trade.entry_side, False)
|
||||
|
@@ -35,9 +35,5 @@ def interest(
|
||||
elif exchange_name == "kraken":
|
||||
# Rounded based on https://kraken-fees-calculator.github.io/
|
||||
return borrowed * rate * (one + FtPrecise(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 * FtPrecise(ceil(hours)) / twenty_four
|
||||
else:
|
||||
raise OperationalException(f"Leverage not available on {exchange_name} with freqtrade")
|
||||
|
@@ -166,7 +166,7 @@ class Backtesting:
|
||||
PairLocks.use_db = True
|
||||
Trade.use_db = True
|
||||
|
||||
def init_backtest_detail(self):
|
||||
def init_backtest_detail(self) -> None:
|
||||
# Load detail timeframe if specified
|
||||
self.timeframe_detail = str(self.config.get('timeframe_detail', ''))
|
||||
if self.timeframe_detail:
|
||||
@@ -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)
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import logging
|
||||
from typing import List
|
||||
from typing import List, Optional
|
||||
|
||||
from sqlalchemy import inspect, select, text, tuple_, update
|
||||
|
||||
@@ -31,9 +31,9 @@ def get_backup_name(tabs: List[str], backup_prefix: str):
|
||||
return table_back_name
|
||||
|
||||
|
||||
def get_last_sequence_ids(engine, trade_back_name, order_back_name):
|
||||
order_id: int = None
|
||||
trade_id: int = None
|
||||
def get_last_sequence_ids(engine, trade_back_name: str, order_back_name: str):
|
||||
order_id: Optional[int] = None
|
||||
trade_id: Optional[int] = None
|
||||
|
||||
if engine.name == 'postgresql':
|
||||
with engine.begin() as connection:
|
||||
|
@@ -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
|
||||
|
@@ -84,11 +84,8 @@ async def _process_consumer_request(
|
||||
# Limit the amount of candles per dataframe to 'limit' or 1500
|
||||
limit = max(data.get('limit', 1500), 1500)
|
||||
|
||||
# They requested the full historical analyzed dataframes
|
||||
analyzed_df = rpc._ws_request_analyzed_df(limit)
|
||||
|
||||
# For every dataframe, send as a separate message
|
||||
for _, message in analyzed_df.items():
|
||||
# For every pair in the generator, send a separate message
|
||||
for message in rpc._ws_request_analyzed_df(limit):
|
||||
response = WSAnalyzedDFMessage(data=message)
|
||||
await channel_manager.send_direct(channel, response.dict(exclude_none=True))
|
||||
|
||||
|
@@ -2,7 +2,7 @@ import asyncio
|
||||
import logging
|
||||
from ipaddress import IPv4Address
|
||||
from threading import Thread
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import orjson
|
||||
import uvicorn
|
||||
@@ -51,9 +51,9 @@ class ApiServer(RPCHandler):
|
||||
# Exchange - only available in webserver mode.
|
||||
_exchange = None
|
||||
# websocket message queue stuff
|
||||
_ws_channel_manager = None
|
||||
_ws_channel_manager: ChannelManager
|
||||
_ws_thread = None
|
||||
_ws_loop = None
|
||||
_ws_loop: Optional[asyncio.AbstractEventLoop] = None
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
"""
|
||||
@@ -71,7 +71,7 @@ class ApiServer(RPCHandler):
|
||||
return
|
||||
self._standalone: bool = standalone
|
||||
self._server = None
|
||||
self._ws_queue = None
|
||||
self._ws_queue: Optional[ThreadedQueue] = None
|
||||
self._ws_background_task = None
|
||||
|
||||
ApiServer.__initialized = True
|
||||
@@ -186,7 +186,7 @@ class ApiServer(RPCHandler):
|
||||
self._ws_background_task = asyncio.run_coroutine_threadsafe(
|
||||
self._broadcast_queue_data(), loop=self._ws_loop)
|
||||
|
||||
async def _broadcast_queue_data(self):
|
||||
async def _broadcast_queue_data(self) -> None:
|
||||
# Instantiate the queue in this coroutine so it's attached to our loop
|
||||
self._ws_queue = ThreadedQueue()
|
||||
async_queue = self._ws_queue.async_q
|
||||
@@ -210,7 +210,8 @@ class ApiServer(RPCHandler):
|
||||
finally:
|
||||
# Disconnect channels and stop the loop on cancel
|
||||
await self._ws_channel_manager.disconnect_all()
|
||||
self._ws_loop.stop()
|
||||
if self._ws_loop:
|
||||
self._ws_loop.stop()
|
||||
# Avoid adding more items to the queue if they aren't
|
||||
# going to get broadcasted.
|
||||
self._ws_queue = None
|
||||
|
@@ -5,7 +5,7 @@ import logging
|
||||
from abc import abstractmethod
|
||||
from datetime import date, datetime, timedelta, timezone
|
||||
from math import isnan
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
from typing import Any, Dict, Generator, List, Optional, Tuple, Union
|
||||
|
||||
import arrow
|
||||
import psutil
|
||||
@@ -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
|
||||
@@ -1063,23 +1064,20 @@ class RPC:
|
||||
self,
|
||||
pairlist: List[str],
|
||||
limit: Optional[int]
|
||||
) -> Dict[str, Any]:
|
||||
) -> Generator[Dict[str, Any], None, None]:
|
||||
""" Get the analysed dataframes of each pair in the pairlist """
|
||||
timeframe = self._freqtrade.config['timeframe']
|
||||
candle_type = self._freqtrade.config.get('candle_type_def', CandleType.SPOT)
|
||||
_data = {}
|
||||
|
||||
for pair in pairlist:
|
||||
dataframe, last_analyzed = self.__rpc_analysed_dataframe_raw(pair, timeframe, limit)
|
||||
|
||||
_data[pair] = {
|
||||
yield {
|
||||
"key": (pair, timeframe, candle_type),
|
||||
"df": dataframe,
|
||||
"la": last_analyzed
|
||||
}
|
||||
|
||||
return _data
|
||||
|
||||
def _ws_request_analyzed_df(self, limit: Optional[int]):
|
||||
""" Historical Analyzed Dataframes for WebSocket """
|
||||
whitelist = self._freqtrade.active_pair_whitelist
|
||||
|
@@ -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:
|
||||
|
Reference in New Issue
Block a user