merge develop into feat/shuffle_after_split

This commit is contained in:
robcaulk
2023-02-16 18:46:01 +01:00
203 changed files with 12376 additions and 8494 deletions

View File

@@ -1,19 +1,20 @@
""" Freqtrade bot """
__version__ = '2022.12.dev'
__version__ = '2023.2.dev'
if 'dev' in __version__:
from pathlib import Path
try:
import subprocess
freqtrade_basedir = Path(__file__).parent
__version__ = __version__ + '-' + subprocess.check_output(
['git', 'log', '--format="%h"', '-n 1'],
stderr=subprocess.DEVNULL).decode("utf-8").rstrip().strip('"')
stderr=subprocess.DEVNULL, cwd=freqtrade_basedir).decode("utf-8").rstrip().strip('"')
except Exception: # pragma: no cover
# git not available, ignore
try:
# Try Fallback to freqtrade_commit file (created by CI while building docker image)
from pathlib import Path
versionfile = Path('./freqtrade_commit')
if versionfile.is_file():
__version__ = f"docker-{__version__}-{versionfile.read_text()[:8]}"

View File

@@ -108,7 +108,7 @@ def ask_user_config() -> Dict[str, Any]:
"binance",
"binanceus",
"bittrex",
"gateio",
"gate",
"huobi",
"kraken",
"kucoin",
@@ -123,7 +123,7 @@ def ask_user_config() -> Dict[str, Any]:
"message": "Do you want to trade Perpetual Swaps (perpetual futures)?",
"default": False,
"filter": lambda val: 'futures' if val else 'spot',
"when": lambda x: x["exchange_name"] in ['binance', 'gateio', 'okx'],
"when": lambda x: x["exchange_name"] in ['binance', 'gate', 'okx'],
},
{
"type": "autocomplete",

View File

@@ -251,7 +251,8 @@ AVAILABLE_CLI_OPTIONS = {
"spaces": Arg(
'--spaces',
help='Specify which parameters to hyperopt. Space-separated list.',
choices=['all', 'buy', 'sell', 'roi', 'stoploss', 'trailing', 'protection', 'default'],
choices=['all', 'buy', 'sell', 'roi', 'stoploss',
'trailing', 'protection', 'trades', 'default'],
nargs='+',
default='default',
),
@@ -632,10 +633,11 @@ AVAILABLE_CLI_OPTIONS = {
"1: by enter_tag, "
"2: by enter_tag and exit_tag, "
"3: by pair and enter_tag, "
"4: by pair, enter_ and exit_tag (this can get quite large)"),
"4: by pair, enter_ and exit_tag (this can get quite large), "
"5: by exit_tag"),
nargs='+',
default=['0', '1', '2'],
choices=['0', '1', '2', '3', '4'],
choices=['0', '1', '2', '3', '4', '5'],
),
"enter_reason_list": Arg(
"--enter-reason-list",

View File

@@ -14,6 +14,7 @@ from freqtrade.exceptions import OperationalException
from freqtrade.exchange import market_is_active, timeframe_to_minutes
from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist, expand_pairlist
from freqtrade.resolvers import ExchangeResolver
from freqtrade.util.binance_mig import migrate_binance_futures_data
logger = logging.getLogger(__name__)
@@ -86,6 +87,7 @@ def start_download_data(args: Dict[str, Any]) -> None:
"Please use `--dl-trades` instead for this exchange "
"(will unfortunately take a long time)."
)
migrate_binance_futures_data(config)
pairs_not_available = refresh_backtest_ohlcv_data(
exchange, pairs=expanded_pairs, timeframes=config['timeframes'],
datadir=config['datadir'], timerange=timerange,
@@ -145,6 +147,7 @@ def start_convert_data(args: Dict[str, Any], ohlcv: bool = True) -> None:
"""
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
if ohlcv:
migrate_binance_futures_data(config)
candle_types = [CandleType.from_string(ct) for ct in config.get('candle_types', ['spot'])]
for candle_type in candle_types:
convert_ohlcv_format(config,

View File

@@ -1,4 +1,5 @@
import logging
import signal
from typing import Any, Dict
@@ -12,15 +13,20 @@ def start_trading(args: Dict[str, Any]) -> int:
# Import here to avoid loading worker module when it's not used
from freqtrade.worker import Worker
def term_handler(signum, frame):
# Raise KeyboardInterrupt - so we can handle it in the same way as Ctrl-C
raise KeyboardInterrupt()
# Create and run worker
worker = None
try:
signal.signal(signal.SIGTERM, term_handler)
worker = Worker(args)
worker.run()
except Exception as e:
logger.error(str(e))
logger.exception("Fatal exception!")
except KeyboardInterrupt:
except (KeyboardInterrupt):
logger.info('SIGINT received, aborting ...')
finally:
if worker:

View File

@@ -28,7 +28,7 @@ class Configuration:
Reuse this class for the bot, backtesting, hyperopt and every script that required configuration
"""
def __init__(self, args: Dict[str, Any], runmode: RunMode = None) -> None:
def __init__(self, args: Dict[str, Any], runmode: Optional[RunMode] = None) -> None:
self.args = args
self.config: Optional[Config] = None
self.runmode = runmode

View File

@@ -32,7 +32,7 @@ def flat_vars_to_nested_dict(env_dict: Dict[str, Any], prefix: str) -> Dict[str,
:param prefix: Prefix to consider (usually FREQTRADE__)
:return: Nested dict based on available and relevant variables.
"""
no_convert = ['CHAT_ID']
no_convert = ['CHAT_ID', 'PASSWORD']
relevant_vars: Dict[str, Any] = {}
for env_var, val in sorted(env_dict.items()):

View File

@@ -6,7 +6,7 @@ import re
import sys
from copy import deepcopy
from pathlib import Path
from typing import Any, Dict, List
from typing import Any, Dict, List, Optional
import rapidjson
@@ -75,7 +75,8 @@ def load_config_file(path: str) -> Dict[str, Any]:
return config
def load_from_files(files: List[str], base_path: Path = None, level: int = 0) -> Dict[str, Any]:
def load_from_files(
files: List[str], base_path: Optional[Path] = None, level: int = 0) -> Dict[str, Any]:
"""
Recursively load configuration files if specified.
Sub-files are assumed to be relative to the initial config.

View File

@@ -5,7 +5,7 @@ bot constants
"""
from typing import Any, Dict, List, Literal, Tuple
from freqtrade.enums import CandleType, RPCMessageType
from freqtrade.enums import CandleType, PriceType, RPCMessageType
DEFAULT_CONFIG = 'config.json'
@@ -25,13 +25,14 @@ PRICING_SIDES = ['ask', 'bid', 'same', 'other']
ORDERTYPE_POSSIBILITIES = ['limit', 'market']
_ORDERTIF_POSSIBILITIES = ['GTC', 'FOK', 'IOC', 'PO']
ORDERTIF_POSSIBILITIES = _ORDERTIF_POSSIBILITIES + [t.lower() for t in _ORDERTIF_POSSIBILITIES]
STOPLOSS_PRICE_TYPES = [p for p in PriceType]
HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss',
'SharpeHyperOptLoss', 'SharpeHyperOptLossDaily',
'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily',
'CalmarHyperOptLoss',
'MaxDrawDownHyperOptLoss', 'MaxDrawDownRelativeHyperOptLoss',
'ProfitDrawDownHyperOptLoss']
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'ProducerPairList',
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'ProducerPairList', 'RemotePairList',
'AgeFilter', 'OffsetFilter', 'PerformanceFilter',
'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter',
'ShuffleFilter', 'SpreadFilter', 'VolatilityFilter']
@@ -61,6 +62,7 @@ USERPATH_FREQAIMODELS = 'freqaimodels'
TELEGRAM_SETTING_OPTIONS = ['on', 'off', 'silent']
WEBHOOK_FORMAT_OPTIONS = ['form', 'json', 'raw']
FULL_DATAFRAME_THRESHOLD = 100
ENV_VAR_PREFIX = 'FREQTRADE__'
@@ -228,6 +230,7 @@ CONF_SCHEMA = {
'default': 'market'},
'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
'stoploss_on_exchange': {'type': 'boolean'},
'stoploss_price_type': {'type': 'string', 'enum': STOPLOSS_PRICE_TYPES},
'stoploss_on_exchange_interval': {'type': 'number'},
'stoploss_on_exchange_limit_ratio': {'type': 'number', 'minimum': 0.0,
'maximum': 1.0}
@@ -635,7 +638,6 @@ SCHEMA_TRADE_REQUIRED = [
SCHEMA_BACKTEST_REQUIRED = [
'exchange',
'max_open_trades',
'stake_currency',
'stake_amount',
'dry_run_wallet',
@@ -645,6 +647,7 @@ SCHEMA_BACKTEST_REQUIRED = [
SCHEMA_BACKTEST_REQUIRED_FINAL = SCHEMA_BACKTEST_REQUIRED + [
'stoploss',
'minimal_roi',
'max_open_trades'
]
SCHEMA_MINIMAL_REQUIRED = [
@@ -678,5 +681,7 @@ EntryExit = Literal['entry', 'exit']
BuySell = Literal['buy', 'sell']
MakerTaker = Literal['maker', 'taker']
BidAsk = Literal['bid', 'ask']
OBLiteral = Literal['asks', 'bids']
Config = Dict[str, Any]
IntOrInf = float

View File

@@ -10,7 +10,7 @@ from typing import Any, Dict, List, Optional, Union
import numpy as np
import pandas as pd
from freqtrade.constants import LAST_BT_RESULT_FN
from freqtrade.constants import LAST_BT_RESULT_FN, IntOrInf
from freqtrade.exceptions import OperationalException
from freqtrade.misc import json_load
from freqtrade.optimize.backtest_caching import get_backtest_metadata_filename
@@ -20,8 +20,8 @@ from freqtrade.persistence import LocalTrade, Trade, init_db
logger = logging.getLogger(__name__)
# Newest format
BT_DATA_COLUMNS = ['pair', 'stake_amount', 'amount', 'open_date', 'close_date',
'open_rate', 'close_rate',
BT_DATA_COLUMNS = ['pair', 'stake_amount', 'max_stake_amount', 'amount',
'open_date', 'close_date', 'open_rate', 'close_rate',
'fee_open', 'fee_close', 'trade_duration',
'profit_ratio', 'profit_abs', 'exit_reason',
'initial_stop_loss_abs', 'initial_stop_loss_ratio', 'stop_loss_abs',
@@ -90,7 +90,8 @@ def get_latest_hyperopt_filename(directory: Union[Path, str]) -> str:
return 'hyperopt_results.pickle'
def get_latest_hyperopt_file(directory: Union[Path, str], predef_filename: str = None) -> Path:
def get_latest_hyperopt_file(
directory: Union[Path, str], predef_filename: Optional[str] = None) -> Path:
"""
Get latest hyperopt export based on '.last_result.json'.
:param directory: Directory to search for last result
@@ -193,7 +194,7 @@ def get_backtest_resultlist(dirname: Path):
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: Optional[datetime] = None) -> Dict[str, Any]:
"""
Find existing backtest stats that match specified run IDs and load them.
:param dirname: pathlib.Path object, or string pointing to the file.
@@ -241,6 +242,33 @@ def find_existing_backtest_stats(dirname: Union[Path, str], run_ids: Dict[str, s
return results
def _load_backtest_data_df_compatibility(df: pd.DataFrame) -> pd.DataFrame:
"""
Compatibility support for older backtest data.
"""
df['open_date'] = pd.to_datetime(df['open_date'],
utc=True,
infer_datetime_format=True
)
df['close_date'] = pd.to_datetime(df['close_date'],
utc=True,
infer_datetime_format=True
)
# Compatibility support for pre short Columns
if 'is_short' not in df.columns:
df['is_short'] = False
if 'leverage' not in df.columns:
df['leverage'] = 1.0
if 'enter_tag' not in df.columns:
df['enter_tag'] = df['buy_tag']
df = df.drop(['buy_tag'], axis=1)
if 'max_stake_amount' not in df.columns:
df['max_stake_amount'] = df['stake_amount']
if 'orders' not in df.columns:
df['orders'] = None
return df
def load_backtest_data(filename: Union[Path, str], strategy: Optional[str] = None) -> pd.DataFrame:
"""
Load backtest data file.
@@ -269,24 +297,7 @@ def load_backtest_data(filename: Union[Path, str], strategy: Optional[str] = Non
data = data['strategy'][strategy]['trades']
df = pd.DataFrame(data)
if not df.empty:
df['open_date'] = pd.to_datetime(df['open_date'],
utc=True,
infer_datetime_format=True
)
df['close_date'] = pd.to_datetime(df['close_date'],
utc=True,
infer_datetime_format=True
)
# Compatibility support for pre short Columns
if 'is_short' not in df.columns:
df['is_short'] = 0
if 'leverage' not in df.columns:
df['leverage'] = 1.0
if 'enter_tag' not in df.columns:
df['enter_tag'] = df['buy_tag']
df = df.drop(['buy_tag'], axis=1)
if 'orders' not in df.columns:
df['orders'] = None
df = _load_backtest_data_df_compatibility(df)
else:
# old format - only with lists.
@@ -322,7 +333,7 @@ def analyze_trade_parallelism(results: pd.DataFrame, timeframe: str) -> pd.DataF
def evaluate_result_multi(results: pd.DataFrame, timeframe: str,
max_open_trades: int) -> pd.DataFrame:
max_open_trades: IntOrInf) -> pd.DataFrame:
"""
Find overlapping trades by expanding each trade once per period it was open
and then counting overlaps

View File

@@ -9,14 +9,17 @@ from collections import deque
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional, Tuple
from pandas import DataFrame
from pandas import DataFrame, Timedelta, Timestamp, to_timedelta
from freqtrade.configuration import TimeRange
from freqtrade.constants import Config, ListPairsWithTimeframes, PairWithTimeframe
from freqtrade.constants import (FULL_DATAFRAME_THRESHOLD, Config, ListPairsWithTimeframes,
PairWithTimeframe)
from freqtrade.data.history import load_pair_history
from freqtrade.enums import CandleType, RPCMessageType, RunMode
from freqtrade.exceptions import ExchangeError, OperationalException
from freqtrade.exchange import Exchange, timeframe_to_seconds
from freqtrade.exchange.types import OrderBook
from freqtrade.misc import append_candles_to_dataframe
from freqtrade.rpc import RPCManager
from freqtrade.util import PeriodicCache
@@ -120,7 +123,7 @@ class DataProvider:
'type': RPCMessageType.ANALYZED_DF,
'data': {
'key': pair_key,
'df': dataframe,
'df': dataframe.tail(1),
'la': datetime.now(timezone.utc)
}
}
@@ -131,7 +134,7 @@ class DataProvider:
'data': pair_key,
})
def _add_external_df(
def _replace_external_df(
self,
pair: str,
dataframe: DataFrame,
@@ -157,6 +160,87 @@ class DataProvider:
self.__producer_pairs_df[producer_name][pair_key] = (dataframe, _last_analyzed)
logger.debug(f"External DataFrame for {pair_key} from {producer_name} added.")
def _add_external_df(
self,
pair: str,
dataframe: DataFrame,
last_analyzed: datetime,
timeframe: str,
candle_type: CandleType,
producer_name: str = "default"
) -> Tuple[bool, int]:
"""
Append a candle to the existing external dataframe. The incoming dataframe
must have at least 1 candle.
:param pair: pair to get the data for
:param timeframe: Timeframe to get data for
:param candle_type: Any of the enum CandleType (must match trading mode!)
:returns: False if the candle could not be appended, or the int number of missing candles.
"""
pair_key = (pair, timeframe, candle_type)
if dataframe.empty:
# The incoming dataframe must have at least 1 candle
return (False, 0)
if len(dataframe) >= FULL_DATAFRAME_THRESHOLD:
# This is likely a full dataframe
# Add the dataframe to the dataprovider
self._replace_external_df(
pair,
dataframe,
last_analyzed=last_analyzed,
timeframe=timeframe,
candle_type=candle_type,
producer_name=producer_name
)
return (True, 0)
if (producer_name not in self.__producer_pairs_df
or pair_key not in self.__producer_pairs_df[producer_name]):
# We don't have data from this producer yet,
# or we don't have data for this pair_key
# return False and 1000 for the full df
return (False, 1000)
existing_df, _ = self.__producer_pairs_df[producer_name][pair_key]
# CHECK FOR MISSING CANDLES
# Convert the timeframe to a timedelta for pandas
timeframe_delta: Timedelta = to_timedelta(timeframe)
local_last: Timestamp = existing_df.iloc[-1]['date'] # We want the last date from our copy
# We want the first date from the incoming
incoming_first: Timestamp = dataframe.iloc[0]['date']
# Remove existing candles that are newer than the incoming first candle
existing_df1 = existing_df[existing_df['date'] < incoming_first]
candle_difference = (incoming_first - local_last) / timeframe_delta
# If the difference divided by the timeframe is 1, then this
# is the candle we want and the incoming data isn't missing any.
# If the candle_difference is more than 1, that means
# we missed some candles between our data and the incoming
# so return False and candle_difference.
if candle_difference > 1:
return (False, int(candle_difference))
if existing_df1.empty:
appended_df = dataframe
else:
appended_df = append_candles_to_dataframe(existing_df1, dataframe)
# Everything is good, we appended
self._replace_external_df(
pair,
appended_df,
last_analyzed=last_analyzed,
timeframe=timeframe,
candle_type=candle_type,
producer_name=producer_name
)
return (True, 0)
def get_producer_df(
self,
pair: str,
@@ -200,7 +284,7 @@ class DataProvider:
def historic_ohlcv(
self,
pair: str,
timeframe: str = None,
timeframe: Optional[str] = None,
candle_type: str = ''
) -> DataFrame:
"""
@@ -252,7 +336,7 @@ class DataProvider:
def get_pair_dataframe(
self,
pair: str,
timeframe: str = None,
timeframe: Optional[str] = None,
candle_type: str = ''
) -> DataFrame:
"""
@@ -334,7 +418,7 @@ class DataProvider:
def refresh(self,
pairlist: ListPairsWithTimeframes,
helping_pairs: ListPairsWithTimeframes = None) -> None:
helping_pairs: Optional[ListPairsWithTimeframes] = None) -> None:
"""
Refresh data, called with each cycle
"""
@@ -358,7 +442,7 @@ class DataProvider:
def ohlcv(
self,
pair: str,
timeframe: str = None,
timeframe: Optional[str] = None,
copy: bool = True,
candle_type: str = ''
) -> DataFrame:
@@ -406,7 +490,7 @@ class DataProvider:
except ExchangeError:
return {}
def orderbook(self, pair: str, maximum: int) -> Dict[str, List]:
def orderbook(self, pair: str, maximum: int) -> OrderBook:
"""
Fetch latest l2 orderbook data
Warning: Does a network request - so use with common sense.

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']
@@ -141,6 +141,12 @@ def _do_group_table_output(bigdf, glist):
# 4: profit summaries grouped by pair, enter_ and exit_tag (this can get quite large)
if g == "4":
group_mask = ['pair', 'enter_reason', 'exit_reason']
# 5: profit summaries grouped by exit_tag
if g == "5":
group_mask = ['exit_reason']
sortcols = ['exit_reason']
if group_mask:
new = bigdf.groupby(group_mask).agg(agg_mask).reset_index()
new.columns = group_mask + agg_cols

View File

@@ -28,8 +28,8 @@ def load_pair_history(pair: str,
fill_up_missing: bool = True,
drop_incomplete: bool = False,
startup_candles: int = 0,
data_format: str = None,
data_handler: IDataHandler = None,
data_format: Optional[str] = None,
data_handler: Optional[IDataHandler] = None,
candle_type: CandleType = CandleType.SPOT
) -> DataFrame:
"""
@@ -69,7 +69,7 @@ def load_data(datadir: Path,
fail_without_data: bool = False,
data_format: str = 'json',
candle_type: CandleType = CandleType.SPOT,
user_futures_funding_rate: int = None,
user_futures_funding_rate: Optional[int] = None,
) -> Dict[str, DataFrame]:
"""
Load ohlcv history data for a list of pairs.
@@ -116,7 +116,7 @@ def refresh_data(*, datadir: Path,
timeframe: str,
pairs: List[str],
exchange: Exchange,
data_format: str = None,
data_format: Optional[str] = None,
timerange: Optional[TimeRange] = None,
candle_type: CandleType,
) -> None:
@@ -189,7 +189,7 @@ def _download_pair_history(pair: str, *,
timeframe: str = '5m',
process: str = '',
new_pairs_days: int = 30,
data_handler: IDataHandler = None,
data_handler: Optional[IDataHandler] = None,
timerange: Optional[TimeRange] = None,
candle_type: CandleType,
erase: bool = False,
@@ -272,7 +272,7 @@ def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes
datadir: Path, trading_mode: str,
timerange: Optional[TimeRange] = None,
new_pairs_days: int = 30, erase: bool = False,
data_format: str = None,
data_format: Optional[str] = None,
prepend: bool = False,
) -> List[str]:
"""

View File

@@ -308,7 +308,7 @@ class IDataHandler(ABC):
timerange=timerange_startup,
candle_type=candle_type
)
if self._check_empty_df(pairdf, pair, timeframe, candle_type, warn_no_data, True):
if self._check_empty_df(pairdf, pair, timeframe, candle_type, warn_no_data):
return pairdf
else:
enddate = pairdf.iloc[-1]['date']
@@ -316,7 +316,7 @@ class IDataHandler(ABC):
if timerange_startup:
self._validate_pairdata(pair, pairdf, timeframe, candle_type, timerange_startup)
pairdf = trim_dataframe(pairdf, timerange_startup)
if self._check_empty_df(pairdf, pair, timeframe, candle_type, warn_no_data):
if self._check_empty_df(pairdf, pair, timeframe, candle_type, warn_no_data, True):
return pairdf
# incomplete candles should only be dropped if we didn't trim the end beforehand.
@@ -374,6 +374,21 @@ class IDataHandler(ABC):
logger.warning(f"{pair}, {candle_type}, {timeframe}, "
f"data ends at {pairdata.iloc[-1]['date']:%Y-%m-%d %H:%M:%S}")
def rename_futures_data(
self, pair: str, new_pair: str, timeframe: str, candle_type: CandleType):
"""
Temporary method to migrate data from old naming to new naming (BTC/USDT -> BTC/USDT:USDT)
Only used for binance to support the binance futures naming unification.
"""
file_old = self._pair_data_filename(self._datadir, pair, timeframe, candle_type)
file_new = self._pair_data_filename(self._datadir, new_pair, timeframe, candle_type)
# print(file_old, file_new)
if file_new.exists():
logger.warning(f"{file_new} exists already, can't migrate {pair}.")
return
file_old.rename(file_new)
def get_datahandlerclass(datatype: str) -> Type[IDataHandler]:
"""
@@ -403,8 +418,8 @@ def get_datahandlerclass(datatype: str) -> Type[IDataHandler]:
raise ValueError(f"No datahandler for datatype {datatype} available.")
def get_datahandler(datadir: Path, data_format: str = None,
data_handler: IDataHandler = None) -> IDataHandler:
def get_datahandler(datadir: Path, data_format: Optional[str] = None,
data_handler: Optional[IDataHandler] = None) -> IDataHandler:
"""
:param datadir: Folder to save data
:param data_format: dataformat to use

View File

@@ -1,4 +1,6 @@
import logging
import math
from datetime import datetime
from typing import Dict, Tuple
import numpy as np
@@ -190,3 +192,119 @@ def calculate_cagr(days_passed: int, starting_balance: float, final_balance: flo
:return: CAGR
"""
return (final_balance / starting_balance) ** (1 / (days_passed / 365)) - 1
def calculate_expectancy(trades: pd.DataFrame) -> float:
"""
Calculate expectancy
:param trades: DataFrame containing trades (requires columns close_date and profit_abs)
:return: expectancy
"""
if len(trades) == 0:
return 0
expectancy = 1
profit_sum = trades.loc[trades['profit_abs'] > 0, 'profit_abs'].sum()
loss_sum = abs(trades.loc[trades['profit_abs'] < 0, 'profit_abs'].sum())
nb_win_trades = len(trades.loc[trades['profit_abs'] > 0])
nb_loss_trades = len(trades.loc[trades['profit_abs'] < 0])
if (nb_win_trades > 0) and (nb_loss_trades > 0):
average_win = profit_sum / nb_win_trades
average_loss = loss_sum / nb_loss_trades
risk_reward_ratio = average_win / average_loss
winrate = nb_win_trades / len(trades)
expectancy = ((1 + risk_reward_ratio) * winrate) - 1
elif nb_win_trades == 0:
expectancy = 0
return expectancy
def calculate_sortino(trades: pd.DataFrame, min_date: datetime, max_date: datetime,
starting_balance: float) -> float:
"""
Calculate sortino
:param trades: DataFrame containing trades (requires columns profit_abs)
:return: sortino
"""
if (len(trades) == 0) or (min_date is None) or (max_date is None) or (min_date == max_date):
return 0
total_profit = trades['profit_abs'] / starting_balance
days_period = max(1, (max_date - min_date).days)
expected_returns_mean = total_profit.sum() / days_period
down_stdev = np.std(trades.loc[trades['profit_abs'] < 0, 'profit_abs'] / starting_balance)
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.
sortino_ratio = -100
# print(expected_returns_mean, down_stdev, sortino_ratio)
return sortino_ratio
def calculate_sharpe(trades: pd.DataFrame, min_date: datetime, max_date: datetime,
starting_balance: float) -> float:
"""
Calculate sharpe
:param trades: DataFrame containing trades (requires column profit_abs)
:return: sharpe
"""
if (len(trades) == 0) or (min_date is None) or (max_date is None) or (min_date == max_date):
return 0
total_profit = trades['profit_abs'] / starting_balance
days_period = max(1, (max_date - min_date).days)
expected_returns_mean = total_profit.sum() / days_period
up_stdev = np.std(total_profit)
if up_stdev != 0:
sharp_ratio = expected_returns_mean / up_stdev * np.sqrt(365)
else:
# Define high (negative) sharpe ratio to be clear that this is NOT optimal.
sharp_ratio = -100
# print(expected_returns_mean, up_stdev, sharp_ratio)
return sharp_ratio
def calculate_calmar(trades: pd.DataFrame, min_date: datetime, max_date: datetime,
starting_balance: float) -> float:
"""
Calculate calmar
:param trades: DataFrame containing trades (requires columns close_date and profit_abs)
:return: calmar
"""
if (len(trades) == 0) or (min_date is None) or (max_date is None) or (min_date == max_date):
return 0
total_profit = trades['profit_abs'].sum() / starting_balance
days_period = max(1, (max_date - min_date).days)
# adding slippage of 0.1% per trade
# total_profit = total_profit - 0.0005
expected_returns_mean = total_profit / days_period * 100
# calculate max drawdown
try:
_, _, _, _, _, max_drawdown = calculate_max_drawdown(
trades, value_col="profit_abs", starting_balance=starting_balance
)
except ValueError:
max_drawdown = 0
if max_drawdown != 0:
calmar_ratio = expected_returns_mean / max_drawdown * math.sqrt(365)
else:
# Define high (negative) calmar ratio to be clear that this is NOT optimal.
calmar_ratio = -100
# print(expected_returns_mean, max_drawdown, calmar_ratio)
return calmar_ratio

View File

@@ -195,7 +195,7 @@ class Edge:
def stake_amount(self, pair: str, free_capital: float,
total_capital: float, capital_in_trade: float) -> float:
stoploss = self.stoploss(pair)
stoploss = self.get_stoploss(pair)
available_capital = (total_capital + capital_in_trade) * self._capital_ratio
allowed_capital_at_risk = available_capital * self._allowed_risk
max_position_size = abs(allowed_capital_at_risk / stoploss)
@@ -214,7 +214,7 @@ class Edge:
)
return round(position_size, 15)
def stoploss(self, pair: str) -> float:
def get_stoploss(self, pair: str) -> float:
if pair in self._cached_pairs:
return self._cached_pairs[pair].stoploss
else:

View File

@@ -6,6 +6,7 @@ from freqtrade.enums.exittype import ExitType
from freqtrade.enums.hyperoptstate import HyperoptState
from freqtrade.enums.marginmode import MarginMode
from freqtrade.enums.ordertypevalue import OrderTypeValues
from freqtrade.enums.pricetype import PriceType
from freqtrade.enums.rpcmessagetype import NO_ECHO_MESSAGES, RPCMessageType, RPCRequestType
from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode
from freqtrade.enums.signaltype import SignalDirection, SignalTagType, SignalType

View File

@@ -0,0 +1,8 @@
from enum import Enum
class PriceType(str, Enum):
"""Enum to distinguish possible trigger prices for stoplosses"""
LAST = "last"
MARK = "mark"
INDEX = "index"

View File

@@ -3,7 +3,6 @@
from freqtrade.exchange.common import remove_credentials, MAP_EXCHANGE_CHILDCLASS
from freqtrade.exchange.exchange import Exchange
# isort: on
from freqtrade.exchange.bibox import Bibox
from freqtrade.exchange.binance import Binance
from freqtrade.exchange.bitpanda import Bitpanda
from freqtrade.exchange.bittrex import Bittrex
@@ -18,7 +17,7 @@ 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.gateio import Gateio
from freqtrade.exchange.gate import Gate
from freqtrade.exchange.hitbtc import Hitbtc
from freqtrade.exchange.huobi import Huobi
from freqtrade.exchange.kraken import Kraken

View File

@@ -1,28 +0,0 @@
""" Bibox exchange subclass """
import logging
from typing import Dict
from freqtrade.exchange import Exchange
logger = logging.getLogger(__name__)
class Bibox(Exchange):
"""
Bibox exchange class. Contains adjustments needed for Freqtrade to work
with this exchange.
Please note that this exchange is not included in the list of exchanges
officially supported by the Freqtrade development team. So some features
may still not work as expected.
"""
# fetchCurrencies API point requires authentication for Bibox,
# so switch it off for Freqtrade load_markets()
@property
def _ccxt_config(self) -> Dict:
# Parameters to add directly to ccxt sync/async initialization.
config = {"has": {"fetchCurrencies": False}}
config.update(super()._ccxt_config)
return config

View File

@@ -7,11 +7,11 @@ from typing import Dict, List, Optional, Tuple
import arrow
import ccxt
from freqtrade.enums import CandleType, MarginMode, TradingMode
from freqtrade.enums import CandleType, MarginMode, PriceType, 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
@@ -28,11 +28,16 @@ class Binance(Exchange):
"trades_pagination": "id",
"trades_pagination_arg": "fromId",
"l2_limit_range": [5, 10, 20, 50, 100, 500, 1000],
"ccxt_futures_name": "future"
}
_ft_has_futures: Dict = {
"stoploss_order_types": {"limit": "limit", "market": "market"},
"stoploss_order_types": {"limit": "stop", "market": "stop_market"},
"tickers_have_price": False,
"floor_leverage": True,
"stop_price_type_field": "workingType",
"stop_price_type_value_mapping": {
PriceType.LAST: "CONTRACT_PRICE",
PriceType.MARK: "MARK_PRICE",
},
}
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
@@ -78,33 +83,9 @@ class Binance(Exchange):
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e:
raise OperationalException(e) from e
f'Error in additional_exchange_init due to {e.__class__.__name__}. Message: {e}'
) from e
@retrier
def _set_leverage(
self,
leverage: float,
pair: Optional[str] = None,
trading_mode: Optional[TradingMode] = None
):
"""
Set's the leverage before making a trade, in order to not
have the same leverage on every trade
"""
trading_mode = trading_mode or self.trading_mode
if self._config['dry_run'] or trading_mode != TradingMode.FUTURES:
return
try:
self._api.set_leverage(symbol=pair, leverage=round(leverage))
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e:
raise OperationalException(e) from e
@@ -112,7 +93,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"
@@ -150,6 +131,7 @@ class Binance(Exchange):
is_short: bool,
amount: float,
stake_amount: float,
leverage: float,
wallet_balance: float, # Or margin balance
mm_ex_1: float = 0.0, # (Binance) Cross only
upnl_ex_1: float = 0.0, # (Binance) Cross only
@@ -159,11 +141,12 @@ class Binance(Exchange):
MARGIN: https://www.binance.com/en/support/faq/f6b010588e55413aa58b7d63ee0125ed
PERPETUAL: https://www.binance.com/en/support/faq/b3c689c1f50a44cabb3a84e663b81d93
: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)
:param stake_amount: Stake amount - Collateral in settle currency.
:param leverage: Leverage used for this position.
:param trading_mode: SPOT, MARGIN, FUTURES, etc.
:param margin_mode: Either ISOLATED or CROSS
:param wallet_balance: Amount of margin_mode in the wallet being used to trade

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,16 @@
""" Bybit exchange subclass """
import logging
from typing import Dict, List, Tuple
from datetime import datetime
from typing import Any, Dict, List, Optional, Tuple
from freqtrade.enums import MarginMode, TradingMode
import ccxt
from freqtrade.constants import BuySell
from freqtrade.enums import MarginMode, PriceType, TradingMode
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
from freqtrade.exchange import Exchange
from freqtrade.exchange.common import retrier
from freqtrade.exchange.exchange_utils import timeframe_to_msecs
logger = logging.getLogger(__name__)
@@ -21,17 +28,27 @@ class Bybit(Exchange):
_ft_has: Dict = {
"ohlcv_candle_limit": 1000,
"ccxt_futures_name": "linear",
"ohlcv_has_history": False,
}
_ft_has_futures: Dict = {
"ohlcv_candle_limit": 200,
"ohlcv_has_history": True,
"mark_ohlcv_timeframe": "4h",
"funding_fee_timeframe": "8h",
"stoploss_on_exchange": True,
"stoploss_order_types": {"limit": "limit", "market": "market"},
"stop_price_type_field": "triggerBy",
"stop_price_type_value_mapping": {
PriceType.LAST: "LastPrice",
PriceType.MARK: "MarkPrice",
PriceType.INDEX: "IndexPrice",
},
}
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
# TradingMode.SPOT always supported and not required in this list
# (TradingMode.FUTURES, MarginMode.CROSS),
# (TradingMode.FUTURES, MarginMode.ISOLATED)
(TradingMode.FUTURES, MarginMode.ISOLATED)
]
@property
@@ -47,3 +64,158 @@ class Bybit(Exchange):
})
config.update(super()._ccxt_config)
return config
def market_is_future(self, market: Dict[str, Any]) -> bool:
main = super().market_is_future(market)
# For ByBit, we'll only support USDT markets for now.
return (
main and market['settle'] == 'USDT'
)
@retrier
def additional_exchange_init(self) -> None:
"""
Additional exchange initialization logic.
.api will be available at this point.
Must be overridden in child methods if required.
"""
try:
if self.trading_mode == TradingMode.FUTURES and not self._config['dry_run']:
position_mode = self._api.set_position_mode(False)
self._log_exchange_response('set_position_mode', position_mode)
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Error in additional_exchange_init due to {e.__class__.__name__}. Message: {e}'
) from e
except ccxt.BaseError as e:
raise OperationalException(e) from e
async def _fetch_funding_rate_history(
self,
pair: str,
timeframe: str,
limit: int,
since_ms: Optional[int] = None,
) -> List[List]:
"""
Fetch funding rate history
Necessary workaround until https://github.com/ccxt/ccxt/issues/15990 is fixed.
"""
params = {}
if since_ms:
until = since_ms + (timeframe_to_msecs(timeframe) * self._ft_has['ohlcv_candle_limit'])
params.update({'until': until})
# Funding rate
data = await self._api_async.fetch_funding_rate_history(
pair, since=since_ms,
params=params)
# Convert funding rate to candle pattern
data = [[x['timestamp'], x['fundingRate'], 0, 0, 0, 0] for x in data]
return data
def _lev_prep(self, pair: str, leverage: float, side: BuySell):
if self.trading_mode != TradingMode.SPOT:
params = {'leverage': leverage}
self.set_margin_mode(pair, self.margin_mode, accept_fail=True, params=params)
self._set_leverage(leverage, pair, accept_fail=True)
def _get_params(
self,
side: BuySell,
ordertype: str,
leverage: float,
reduceOnly: bool,
time_in_force: str = 'GTC',
) -> Dict:
params = super()._get_params(
side=side,
ordertype=ordertype,
leverage=leverage,
reduceOnly=reduceOnly,
time_in_force=time_in_force,
)
if self.trading_mode == TradingMode.FUTURES and self.margin_mode:
params['position_idx'] = 0
return params
def dry_run_liquidation_price(
self,
pair: str,
open_rate: float, # Entry price of position
is_short: bool,
amount: float,
stake_amount: float,
leverage: float,
wallet_balance: float, # Or margin balance
mm_ex_1: float = 0.0, # (Binance) Cross only
upnl_ex_1: float = 0.0, # (Binance) Cross only
) -> Optional[float]:
"""
Important: Must be fetching data from cached values as this is used by backtesting!
PERPETUAL:
bybit:
https://www.bybithelp.com/HelpCenterKnowledge/bybitHC_Article?language=en_US&id=000001067
Long:
Liquidation Price = (
Entry Price * (1 - Initial Margin Rate + Maintenance Margin Rate)
- Extra Margin Added/ Contract)
Short:
Liquidation Price = (
Entry Price * (1 + Initial Margin Rate - Maintenance Margin Rate)
+ Extra Margin Added/ Contract)
Implementation Note: Extra margin is currently not used.
: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)
:param stake_amount: Stake amount - Collateral in settle currency.
:param leverage: Leverage used for this position.
:param trading_mode: SPOT, MARGIN, FUTURES, etc.
:param margin_mode: Either ISOLATED or CROSS
:param wallet_balance: Amount of margin_mode in the wallet being used to trade
Cross-Margin Mode: crossWalletBalance
Isolated-Margin Mode: isolatedWalletBalance
"""
market = self.markets[pair]
mm_ratio, _ = self.get_maintenance_ratio_and_amt(pair, stake_amount)
if self.trading_mode == TradingMode.FUTURES and self.margin_mode == MarginMode.ISOLATED:
if market['inverse']:
raise OperationalException(
"Freqtrade does not yet support inverse contracts")
initial_margin_rate = 1 / leverage
# See docstring - ignores extra margin!
if is_short:
return open_rate * (1 + initial_margin_rate - mm_ratio)
else:
return open_rate * (1 - initial_margin_rate + mm_ratio)
else:
raise OperationalException(
"Freqtrade only supports isolated futures for leverage trading")
def get_funding_fees(
self, pair: str, amount: float, is_short: bool, open_date: datetime) -> float:
"""
Fetch funding fees, either from the exchange (live) or calculates them
based on funding rate/mark price history
:param pair: The quote/base pair of the trade
:param is_short: trade direction
:param amount: Trade amount
:param open_date: Open date of the trade
:return: funding fee since open_date
:raises: ExchangeError if something goes wrong.
"""
# Bybit does not provide "applied" funding fees per position.
if self.trading_mode == TradingMode.FUTURES:
return self._fetch_and_calculate_funding_fees(
pair, amount, is_short, open_date)
return 0.0

View File

@@ -46,13 +46,13 @@ MAP_EXCHANGE_CHILDCLASS = {
'binanceje': 'binance',
'binanceusdm': 'binance',
'okex': 'okx',
'gate': 'gateio',
'gateio': 'gate',
}
SUPPORTED_EXCHANGES = [
'binance',
'bittrex',
'gateio',
'gate',
'huobi',
'kraken',
'okx',

View File

@@ -3,11 +3,11 @@
Cryptocurrency Exchanges support
"""
import asyncio
import http
import inspect
import logging
from copy import deepcopy
from datetime import datetime, timedelta, timezone
from math import floor
from threading import Lock
from typing import Any, Coroutine, Dict, List, Literal, Optional, Tuple, Union
@@ -21,9 +21,10 @@ from pandas import DataFrame, concat
from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES, BidAsk,
BuySell, Config, EntryExit, ListPairsWithTimeframes, MakerTaker,
PairWithTimeframe)
OBLiteral, PairWithTimeframe)
from freqtrade.data.converter import clean_ohlcv_dataframe, ohlcv_to_dataframe, trades_dict_to_list
from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, TradingMode
from freqtrade.enums.pricetype import PriceType
from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError,
InvalidOrderException, OperationalException, PricingError,
RetryableOrderError, TemporaryError)
@@ -36,7 +37,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, OrderBook, 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
@@ -45,12 +46,6 @@ from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
logger = logging.getLogger(__name__)
# Workaround for adding samesite support to pre 3.8 python
# Only applies to python3.7, and only on certain exchanges (kraken)
# Replicates the fix from starlette (which is actually causing this problem)
http.cookies.Morsel._reserved["samesite"] = "SameSite" # type: ignore
class Exchange:
# Parameters to add directly to buy/sell calls (like agreeing to trading agreement)
@@ -474,7 +469,7 @@ class Exchange:
try:
if self._api_async:
self.loop.run_until_complete(
self._api_async.load_markets(reload=reload))
self._api_async.load_markets(reload=reload, params={}))
except (asyncio.TimeoutError, ccxt.BaseError) as e:
logger.warning('Could not load async markets. Reason: %s', e)
@@ -483,7 +478,7 @@ class Exchange:
def _load_markets(self) -> None:
""" Initialize markets both sync and async """
try:
self._markets = self._api.load_markets()
self._markets = self._api.load_markets(params={})
self._load_async_markets()
self._last_markets_refresh = arrow.utcnow().int_timestamp
if self._ft_has['needs_trading_fees']:
@@ -501,7 +496,7 @@ class Exchange:
return None
logger.debug("Performing scheduled market reload..")
try:
self._markets = self._api.load_markets(reload=True)
self._markets = self._api.load_markets(reload=True, params={})
# Also reload async markets to avoid issues with newly listed pairs
self._load_async_markets(reload=True)
self._last_markets_refresh = arrow.utcnow().int_timestamp
@@ -606,12 +601,27 @@ class Exchange:
if not self.exchange_has('createMarketOrder'):
raise OperationalException(
f'Exchange {self.name} does not support market orders.')
self.validate_stop_ordertypes(order_types)
def validate_stop_ordertypes(self, order_types: Dict) -> None:
"""
Validate stoploss order types
"""
if (order_types.get("stoploss_on_exchange")
and not self._ft_has.get("stoploss_on_exchange", False)):
raise OperationalException(
f'On exchange stoploss is not supported for {self.name}.'
)
if self.trading_mode == TradingMode.FUTURES:
price_mapping = self._ft_has.get('stop_price_type_value_mapping', {}).keys()
if (
order_types.get("stoploss_on_exchange", False) is True
and 'stoploss_price_type' in order_types
and order_types['stoploss_price_type'] not in price_mapping
):
raise OperationalException(
f'On exchange stoploss price type is not supported for {self.name}.'
)
def validate_pricing(self, pricing: Dict) -> None:
if pricing.get('use_order_book', False) and not self.exchange_has('fetchL2OrderBook'):
@@ -682,7 +692,7 @@ class Exchange:
f"Freqtrade does not support {mm_value} {trading_mode.value} on {self.name}"
)
def get_option(self, param: str, default: Any = None) -> Any:
def get_option(self, param: str, default: Optional[Any] = None) -> Any:
"""
Get parameter value from _ft_has
"""
@@ -850,10 +860,13 @@ class Exchange:
dry_order["stopPrice"] = dry_order["price"]
# Workaround to avoid filling stoploss orders immediately
dry_order["ft_order_type"] = "stoploss"
orderbook: Optional[OrderBook] = None
if self.exchange_has('fetchL2OrderBook'):
orderbook = self.fetch_l2_order_book(pair, 20)
if dry_order["type"] == "market" and not dry_order.get("ft_order_type"):
# Update market order pricing
average = self.get_dry_market_fill_price(pair, side, amount, rate)
average = self.get_dry_market_fill_price(pair, side, amount, rate, orderbook)
dry_order.update({
'average': average,
'filled': _amount,
@@ -863,7 +876,8 @@ class Exchange:
# market orders will always incurr taker fees
dry_order = self.add_dry_order_fee(pair, dry_order, 'taker')
dry_order = self.check_dry_limit_order_filled(dry_order, immediate=True)
dry_order = self.check_dry_limit_order_filled(
dry_order, immediate=True, orderbook=orderbook)
self._dry_run_open_orders[dry_order["id"]] = dry_order
# Copy order and close it - so the returned order is open unless it's a market order
@@ -885,20 +899,22 @@ class Exchange:
})
return dry_order
def get_dry_market_fill_price(self, pair: str, side: str, amount: float, rate: float) -> float:
def get_dry_market_fill_price(self, pair: str, side: str, amount: float, rate: float,
orderbook: Optional[OrderBook]) -> float:
"""
Get the market order fill price based on orderbook interpolation
"""
if self.exchange_has('fetchL2OrderBook'):
ob = self.fetch_l2_order_book(pair, 20)
ob_type = 'asks' if side == 'buy' else 'bids'
if not orderbook:
orderbook = self.fetch_l2_order_book(pair, 20)
ob_type: OBLiteral = 'asks' if side == 'buy' else 'bids'
slippage = 0.05
max_slippage_val = rate * ((1 + slippage) if side == 'buy' else (1 - slippage))
remaining_amount = amount
filled_amount = 0.0
book_entry_price = 0.0
for book_entry in ob[ob_type]:
for book_entry in orderbook[ob_type]:
book_entry_price = book_entry[0]
book_entry_coin_volume = book_entry[1]
if remaining_amount > 0:
@@ -926,18 +942,20 @@ class Exchange:
return rate
def _is_dry_limit_order_filled(self, pair: str, side: str, limit: float) -> bool:
def _is_dry_limit_order_filled(self, pair: str, side: str, limit: float,
orderbook: Optional[OrderBook] = None) -> bool:
if not self.exchange_has('fetchL2OrderBook'):
return True
ob = self.fetch_l2_order_book(pair, 1)
if not orderbook:
orderbook = self.fetch_l2_order_book(pair, 1)
try:
if side == 'buy':
price = ob['asks'][0][0]
price = orderbook['asks'][0][0]
logger.debug(f"{pair} checking dry buy-order: price={price}, limit={limit}")
if limit >= price:
return True
else:
price = ob['bids'][0][0]
price = orderbook['bids'][0][0]
logger.debug(f"{pair} checking dry sell-order: price={price}, limit={limit}")
if limit <= price:
return True
@@ -947,7 +965,8 @@ class Exchange:
return False
def check_dry_limit_order_filled(
self, order: Dict[str, Any], immediate: bool = False) -> Dict[str, Any]:
self, order: Dict[str, Any], immediate: bool = False,
orderbook: Optional[OrderBook] = None) -> Dict[str, Any]:
"""
Check dry-run limit order fill and update fee (if it filled).
"""
@@ -955,7 +974,7 @@ class Exchange:
and order['type'] in ["limit"]
and not order.get('ft_order_type')):
pair = order['symbol']
if self._is_dry_limit_order_filled(pair, order['side'], order['price']):
if self._is_dry_limit_order_filled(pair, order['side'], order['price'], orderbook):
order.update({
'status': 'closed',
'filled': order['amount'],
@@ -1121,8 +1140,8 @@ class Exchange:
return params
@retrier(retries=0)
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict,
side: BuySell, leverage: float) -> Dict:
def create_stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict,
side: BuySell, leverage: float) -> Dict:
"""
creates a stoploss order.
requires `_ft_has['stoploss_order_types']` to be set as a dict mapping limit and market
@@ -1167,6 +1186,10 @@ class Exchange:
stop_price=stop_price_norm)
if self.trading_mode == TradingMode.FUTURES:
params['reduceOnly'] = True
if 'stoploss_price_type' in order_types and 'stop_price_type_field' in self._ft_has:
price_type = self._ft_has['stop_price_type_value_mapping'][
order_types.get('stoploss_price_type', PriceType.LAST)]
params[self._ft_has['stop_price_type_field']] = price_type
amount = self.amount_to_precision(pair, self._amount_to_contracts(pair, amount))
@@ -1357,7 +1380,7 @@ class Exchange:
raise OperationalException(e) from e
@retrier
def fetch_positions(self, pair: str = None) -> List[Dict]:
def fetch_positions(self, pair: Optional[str] = None) -> List[Dict]:
"""
Fetch positions from the exchange.
If no pair is given, all positions are returned.
@@ -1497,7 +1520,7 @@ class Exchange:
return result
@retrier
def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict:
def fetch_l2_order_book(self, pair: str, limit: int = 100) -> OrderBook:
"""
Get L2 order book from exchange.
Can be limited to a certain amount (if supported).
@@ -1540,7 +1563,7 @@ class Exchange:
def get_rate(self, pair: str, refresh: bool,
side: EntryExit, is_short: bool,
order_book: Optional[dict] = None, ticker: Optional[Ticker] = None) -> float:
order_book: Optional[OrderBook] = None, ticker: Optional[Ticker] = None) -> float:
"""
Calculates bid/ask target
bid rate - between current ask price and last price
@@ -1578,7 +1601,8 @@ class Exchange:
logger.debug('order_book %s', order_book)
# top 1 = index 0
try:
rate = order_book[f"{price_side}s"][order_book_top - 1][0]
obside: OBLiteral = 'bids' if price_side == 'bid' else 'asks'
rate = order_book[obside][order_book_top - 1][0]
except (IndexError, KeyError) as e:
logger.warning(
f"{pair} - {name} Price at location {order_book_top} from orderbook "
@@ -1705,7 +1729,7 @@ class Exchange:
return self._config['fee']
# validate that markets are loaded before trying to get fee
if self._api.markets is None or len(self._api.markets) == 0:
self._api.load_markets()
self._api.load_markets(params={})
return self._api.calculate_fee(symbol=symbol, type=type, side=side, amount=amount,
price=price, takerOrMaker=taker_or_maker)['rate']
@@ -1801,7 +1825,7 @@ class Exchange:
def get_historic_ohlcv(self, pair: str, timeframe: str,
since_ms: int, candle_type: CandleType,
is_new_pair: bool = False,
until_ms: int = None) -> List:
until_ms: Optional[int] = None) -> List:
"""
Get candle history using asyncio and returns the list of candles.
Handles all async work for this.
@@ -1813,32 +1837,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 +1879,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 +1925,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 +1989,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 +2006,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 +2036,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
@@ -2035,8 +2046,8 @@ class Exchange:
# Fetch OHLCV asynchronously
s = '(' + arrow.get(since_ms // 1000).isoformat() + ') ' if since_ms is not None else ''
logger.debug(
"Fetching pair %s, interval %s, since %s %s...",
pair, timeframe, since_ms, s
"Fetching pair %s, %s, interval %s, since %s %s...",
pair, candle_type, timeframe, since_ms, s
)
params = deepcopy(self._ft_has.get('ohlcv_params', {}))
candle_limit = self.ohlcv_candle_limit(
@@ -2050,11 +2061,12 @@ class Exchange:
limit=candle_limit, params=params)
else:
# Funding rate
data = await self._api_async.fetch_funding_rate_history(
pair, since=since_ms,
limit=candle_limit)
# Convert funding rate to candle pattern
data = [[x['timestamp'], x['fundingRate'], 0, 0, 0, 0] for x in data]
data = await self._fetch_funding_rate_history(
pair=pair,
timeframe=timeframe,
limit=candle_limit,
since_ms=since_ms,
)
# Some exchanges sort OHLCV in ASC order and others in DESC.
# Ex: Bittrex returns the list of OHLCV in ASC order (oldest first, newest last)
# while GDAX returns the list of OHLCV in DESC order (newest first, oldest last)
@@ -2064,9 +2076,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(
@@ -2082,6 +2094,24 @@ class Exchange:
raise OperationalException(f'Could not fetch historical candle (OHLCV) data '
f'for pair {pair}. Message: {e}') from e
async def _fetch_funding_rate_history(
self,
pair: str,
timeframe: str,
limit: int,
since_ms: Optional[int] = None,
) -> List[List]:
"""
Fetch funding rate history - used to selectively override this by subclasses.
"""
# Funding rate
data = await self._api_async.fetch_funding_rate_history(
pair, since=since_ms,
limit=limit)
# Convert funding rate to candle pattern
data = [[x['timestamp'], x['fundingRate'], 0, 0, 0, 0] for x in data]
return data
# Fetch historic trades
@retrier_async
@@ -2485,7 +2515,8 @@ class Exchange:
self,
leverage: float,
pair: Optional[str] = None,
trading_mode: Optional[TradingMode] = None
trading_mode: Optional[TradingMode] = None,
accept_fail: bool = False,
):
"""
Set's the leverage before making a trade, in order to not
@@ -2494,12 +2525,18 @@ class Exchange:
if self._config['dry_run'] or not self.exchange_has("setLeverage"):
# Some exchanges only support one margin_mode type
return
if self._ft_has.get('floor_leverage', False) is True:
# Rounding for binance ...
leverage = floor(leverage)
try:
res = self._api.set_leverage(symbol=pair, leverage=leverage)
self._log_exchange_response('set_leverage', res)
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except ccxt.BadRequest as e:
if not accept_fail:
raise TemporaryError(
f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e
@@ -2521,7 +2558,8 @@ class Exchange:
return open_date.minute > 0 or open_date.second > 0
@retrier
def set_margin_mode(self, pair: str, margin_mode: MarginMode, params: dict = {}):
def set_margin_mode(self, pair: str, margin_mode: MarginMode, accept_fail: bool = False,
params: dict = {}):
"""
Set's the margin mode on the exchange to cross or isolated for a specific pair
:param pair: base/quote currency pair (e.g. "ADA/USDT")
@@ -2535,6 +2573,10 @@ class Exchange:
self._log_exchange_response('set_margin_mode', res)
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except ccxt.BadRequest as e:
if not accept_fail:
raise TemporaryError(
f'Could not set margin mode due to {e.__class__.__name__}. Message: {e}') from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not set margin mode due to {e.__class__.__name__}. Message: {e}') from e
@@ -2668,7 +2710,7 @@ class Exchange:
:param amount: Trade amount
:param open_date: Open date of the trade
:return: funding fee since open_date
:raies: ExchangeError if something goes wrong.
:raises: ExchangeError if something goes wrong.
"""
if self.trading_mode == TradingMode.FUTURES:
if self._config['dry_run']:
@@ -2688,6 +2730,7 @@ class Exchange:
is_short: bool,
amount: float, # Absolute value of position size
stake_amount: float,
leverage: float,
wallet_balance: float,
mm_ex_1: float = 0.0, # (Binance) Cross only
upnl_ex_1: float = 0.0, # (Binance) Cross only
@@ -2709,6 +2752,7 @@ class Exchange:
open_rate=open_rate,
is_short=is_short,
amount=amount,
leverage=leverage,
stake_amount=stake_amount,
wallet_balance=wallet_balance,
mm_ex_1=mm_ex_1,
@@ -2720,7 +2764,7 @@ class Exchange:
pos = positions[0]
isolated_liq = pos['liquidationPrice']
if isolated_liq:
if isolated_liq is not None:
buffer_amount = abs(open_rate - isolated_liq) * self.liquidation_buffer
isolated_liq = (
isolated_liq - buffer_amount
@@ -2738,6 +2782,7 @@ class Exchange:
is_short: bool,
amount: float,
stake_amount: float,
leverage: float,
wallet_balance: float, # Or margin balance
mm_ex_1: float = 0.0, # (Binance) Cross only
upnl_ex_1: float = 0.0, # (Binance) Cross only
@@ -2745,22 +2790,28 @@ 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
gate: 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)
:param stake_amount: Stake amount - Collateral in settle currency.
:param leverage: Leverage used for this position.
:param trading_mode: SPOT, MARGIN, FUTURES, etc.
:param margin_mode: Either ISOLATED or CROSS
:param wallet_balance: Amount of margin_mode in the wallet being used to trade
Cross-Margin Mode: crossWalletBalance
Isolated-Margin Mode: isolatedWalletBalance
# * Not required by Gateio or OKX
# * Not required by Gate or OKX
:param mm_ex_1:
:param upnl_ex_1:
"""
@@ -2789,7 +2840,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

@@ -15,18 +15,19 @@ from freqtrade.util import FtPrecise
CcxtModuleType = Any
def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool:
def is_exchange_known_ccxt(
exchange_name: str, ccxt_module: Optional[CcxtModuleType] = None) -> bool:
return exchange_name in ccxt_exchanges(ccxt_module)
def ccxt_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]:
def ccxt_exchanges(ccxt_module: Optional[CcxtModuleType] = None) -> List[str]:
"""
Return the list of all exchanges known to ccxt
"""
return ccxt_module.exchanges if ccxt_module is not None else ccxt.exchanges
def available_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]:
def available_exchanges(ccxt_module: Optional[CcxtModuleType] = None) -> List[str]:
"""
Return exchanges available to the bot, i.e. non-bad exchanges in the ccxt list
"""
@@ -86,7 +87,7 @@ def timeframe_to_msecs(timeframe: str) -> int:
return ccxt.Exchange.parse_timeframe(timeframe) * 1000
def timeframe_to_prev_date(timeframe: str, date: datetime = None) -> datetime:
def timeframe_to_prev_date(timeframe: str, date: Optional[datetime] = None) -> datetime:
"""
Use Timeframe and determine the candle start date for this date.
Does not round when given a candle start date.
@@ -102,7 +103,7 @@ def timeframe_to_prev_date(timeframe: str, date: datetime = None) -> datetime:
return datetime.fromtimestamp(new_timestamp, tz=timezone.utc)
def timeframe_to_next_date(timeframe: str, date: datetime = None) -> datetime:
def timeframe_to_next_date(timeframe: str, date: Optional[datetime] = None) -> datetime:
"""
Use Timeframe and determine next candle.
:param timeframe: timeframe in string format (e.g. "5m")

View File

@@ -4,7 +4,7 @@ from datetime import datetime
from typing import Any, Dict, List, Optional, Tuple
from freqtrade.constants import BuySell
from freqtrade.enums import MarginMode, TradingMode
from freqtrade.enums import MarginMode, PriceType, TradingMode
from freqtrade.exceptions import OperationalException
from freqtrade.exchange import Exchange
from freqtrade.misc import safe_value_fallback2
@@ -13,7 +13,7 @@ from freqtrade.misc import safe_value_fallback2
logger = logging.getLogger(__name__)
class Gateio(Exchange):
class Gate(Exchange):
"""
Gate.io exchange class. Contains adjustments needed for Freqtrade to work
with this exchange.
@@ -34,6 +34,12 @@ class Gateio(Exchange):
"needs_trading_fees": True,
"fee_cost_in_contracts": False, # Set explicitly to false for clarity
"order_props_in_contracts": ['amount', 'filled', 'remaining'],
"stop_price_type_field": "price_type",
"stop_price_type_value_mapping": {
PriceType.LAST: 0,
PriceType.MARK: 1,
PriceType.INDEX: 2,
},
}
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
@@ -49,6 +55,7 @@ class Gateio(Exchange):
if any(v == 'market' for k, v in order_types.items()):
raise OperationalException(
f'Exchange {self.name} does not support market orders.')
super().validate_stop_ordertypes(order_types)
def _get_params(
self,
@@ -77,7 +84,7 @@ class Gateio(Exchange):
if self.trading_mode == TradingMode.FUTURES:
# Futures usually don't contain fees in the response.
# As such, futures orders on gateio will not contain a fee, which causes
# As such, futures orders on gate will not contain a fee, which causes
# a repeated "update fee" cycle and wrong calculations.
# Therefore we patch the response with fees if it's not available.
# An alternative also contianing fees would be

View File

@@ -97,8 +97,8 @@ class Kraken(Exchange):
))
@retrier(retries=0)
def stoploss(self, pair: str, amount: float, stop_price: float,
order_types: Dict, side: BuySell, leverage: float) -> Dict:
def create_stoploss(self, pair: str, amount: float, stop_price: float,
order_types: Dict, side: BuySell, leverage: float) -> Dict:
"""
Creates a stoploss market order.
Stoploss market orders is the only stoploss type supported by kraken.
@@ -158,7 +158,8 @@ class Kraken(Exchange):
self,
leverage: float,
pair: Optional[str] = None,
trading_mode: Optional[TradingMode] = None
trading_mode: Optional[TradingMode] = None,
accept_fail: bool = False,
):
"""
Kraken set's the leverage as an option in the order object, so we need to

View File

@@ -36,3 +36,34 @@ class Kucoin(Exchange):
'stop': 'loss'
})
return params
def create_order(
self,
*,
pair: str,
ordertype: str,
side: BuySell,
amount: float,
rate: float,
leverage: float,
reduceOnly: bool = False,
time_in_force: str = 'GTC',
) -> Dict:
res = super().create_order(
pair=pair,
ordertype=ordertype,
side=side,
amount=amount,
rate=rate,
leverage=leverage,
reduceOnly=reduceOnly,
time_in_force=time_in_force,
)
# Kucoin returns only the order-id.
# ccxt returns status = 'closed' at the moment - which is information ccxt invented.
# Since we rely on status heavily, we must set it to 'open' here.
# ref: https://github.com/ccxt/ccxt/pull/16674, (https://github.com/ccxt/ccxt/pull/16553)
res['type'] = ordertype
res['status'] = 'open'
return res

View File

@@ -5,6 +5,7 @@ import ccxt
from freqtrade.constants import BuySell
from freqtrade.enums import CandleType, MarginMode, TradingMode
from freqtrade.enums.pricetype import PriceType
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
from freqtrade.exchange import Exchange, date_minus_candles
from freqtrade.exchange.common import retrier
@@ -27,6 +28,12 @@ class Okx(Exchange):
_ft_has_futures: Dict = {
"tickers_have_quoteVolume": False,
"fee_cost_in_contracts": True,
"stop_price_type_field": "tpTriggerPxType",
"stop_price_type_value_mapping": {
PriceType.LAST: "last",
PriceType.MARK: "index",
PriceType.INDEX: "mark",
},
}
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
@@ -118,13 +125,15 @@ class Okx(Exchange):
if self.trading_mode != TradingMode.SPOT and self.margin_mode is not None:
try:
# TODO-lev: Test me properly (check mgnMode passed)
self._api.set_leverage(
res = self._api.set_leverage(
leverage=leverage,
symbol=pair,
params={
"mgnMode": self.margin_mode.value,
"posSide": self._get_posSide(side, False),
})
self._log_exchange_response('set_leverage', res)
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e:

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):
@@ -13,4 +15,16 @@ class Ticker(TypedDict):
# Several more - only listing required.
class OrderBook(TypedDict):
symbol: str
bids: List[Tuple[float, float]]
asks: List[Tuple[float, float]]
timestamp: Optional[int]
datetime: Optional[str]
nonce: Optional[int]
Tickers = Dict[str, Ticker]
# pair, timeframe, candleType, OHLCV, drop last?,
OHLCVResponse = Tuple[str, str, CandleType, List, bool]

View File

@@ -0,0 +1,125 @@
import logging
from enum import Enum
from gym import spaces
from freqtrade.freqai.RL.BaseEnvironment import BaseEnvironment, Positions
logger = logging.getLogger(__name__)
class Actions(Enum):
Neutral = 0
Buy = 1
Sell = 2
class Base3ActionRLEnv(BaseEnvironment):
"""
Base class for a 3 action environment
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.actions = Actions
def set_action_space(self):
self.action_space = spaces.Discrete(len(Actions))
def step(self, action: int):
"""
Logic for a single step (incrementing one candle in time)
by the agent
:param: action: int = the action type that the agent plans
to take for the current step.
:returns:
observation = current state of environment
step_reward = the reward from `calculate_reward()`
_done = if the agent "died" or if the candles finished
info = dict passed back to openai gym lib
"""
self._done = False
self._current_tick += 1
if self._current_tick == self._end_tick:
self._done = True
self._update_unrealized_total_profit()
step_reward = self.calculate_reward(action)
self.total_reward += step_reward
self.tensorboard_log(self.actions._member_names_[action])
trade_type = None
if self.is_tradesignal(action):
if action == Actions.Buy.value:
if self._position == Positions.Short:
self._update_total_profit()
self._position = Positions.Long
trade_type = "long"
self._last_trade_tick = self._current_tick
elif action == Actions.Sell.value and self.can_short:
if self._position == Positions.Long:
self._update_total_profit()
self._position = Positions.Short
trade_type = "short"
self._last_trade_tick = self._current_tick
elif action == Actions.Sell.value and not self.can_short:
self._update_total_profit()
self._position = Positions.Neutral
trade_type = "neutral"
self._last_trade_tick = None
else:
print("case not defined")
if trade_type is not None:
self.trade_history.append(
{'price': self.current_price(), 'index': self._current_tick,
'type': trade_type})
if (self._total_profit < self.max_drawdown or
self._total_unrealized_profit < self.max_drawdown):
self._done = True
self._position_history.append(self._position)
info = dict(
tick=self._current_tick,
action=action,
total_reward=self.total_reward,
total_profit=self._total_profit,
position=self._position.value,
trade_duration=self.get_trade_duration(),
current_profit_pct=self.get_unrealized_profit()
)
observation = self._get_observation()
self._update_history(info)
return observation, step_reward, self._done, info
def is_tradesignal(self, action: int) -> bool:
"""
Determine if the signal is a trade signal
e.g.: agent wants a Actions.Buy while it is in a Positions.short
"""
return (
(action == Actions.Buy.value and self._position == Positions.Neutral)
or (action == Actions.Sell.value and self._position == Positions.Long)
or (action == Actions.Sell.value and self._position == Positions.Neutral
and self.can_short)
or (action == Actions.Buy.value and self._position == Positions.Short
and self.can_short)
)
def _is_valid(self, action: int) -> bool:
"""
Determine if the signal is valid.
e.g.: agent wants a Actions.Sell while it is in a Positions.Long
"""
if self.can_short:
return action in [Actions.Buy.value, Actions.Sell.value, Actions.Neutral.value]
else:
if action == Actions.Sell.value and self._position != Positions.Long:
return False
return True

View File

@@ -88,7 +88,8 @@ class Base4ActionRLEnv(BaseEnvironment):
{'price': self.current_price(), 'index': self._current_tick,
'type': trade_type})
if self._total_profit < 1 - self.rl_config.get('max_training_drawdown_pct', 0.8):
if (self._total_profit < self.max_drawdown or
self._total_unrealized_profit < self.max_drawdown):
self._done = True
self._position_history.append(self._position)

View File

@@ -45,7 +45,8 @@ class BaseEnvironment(gym.Env):
def __init__(self, df: DataFrame = DataFrame(), prices: DataFrame = DataFrame(),
reward_kwargs: dict = {}, window_size=10, starting_point=True,
id: str = 'baseenv-1', seed: int = 1, config: dict = {}, live: bool = False,
fee: float = 0.0015):
fee: float = 0.0015, can_short: bool = False, pair: str = "",
df_raw: DataFrame = DataFrame()):
"""
Initializes the training/eval environment.
:param df: dataframe of features
@@ -58,13 +59,16 @@ class BaseEnvironment(gym.Env):
:param config: Typical user configuration file
:param live: Whether or not this environment is active in dry/live/backtesting
:param fee: The fee to use for environmental interactions.
:param can_short: Whether or not the environment can short
"""
self.config = config
self.rl_config = config['freqai']['rl_config']
self.add_state_info = self.rl_config.get('add_state_info', False)
self.id = id
self.max_drawdown = 1 - self.rl_config.get('max_training_drawdown_pct', 0.8)
self.compound_trades = config['stake_amount'] == 'unlimited'
self.config: dict = config
self.rl_config: dict = config['freqai']['rl_config']
self.add_state_info: bool = self.rl_config.get('add_state_info', False)
self.id: str = id
self.max_drawdown: float = 1 - self.rl_config.get('max_training_drawdown_pct', 0.8)
self.compound_trades: bool = config['stake_amount'] == 'unlimited'
self.pair: str = pair
self.raw_features: DataFrame = df_raw
if self.config.get('fee', None) is not None:
self.fee = self.config['fee']
else:
@@ -73,7 +77,8 @@ class BaseEnvironment(gym.Env):
# set here to default 5Ac, but all children envs can override this
self.actions: Type[Enum] = BaseActions
self.tensorboard_metrics: dict = {}
self.live = live
self.can_short: bool = can_short
self.live: bool = live
if not self.live and self.add_state_info:
self.add_state_info = False
logger.warning("add_state_info is not available in backtesting. Deactivating.")
@@ -91,13 +96,12 @@ class BaseEnvironment(gym.Env):
:param reward_kwargs: extra config settings assigned by user in `rl_config`
:param starting_point: start at edge of window or not
"""
self.df = df
self.signal_features = self.df
self.prices = prices
self.window_size = window_size
self.starting_point = starting_point
self.rr = reward_kwargs["rr"]
self.profit_aim = reward_kwargs["profit_aim"]
self.signal_features: DataFrame = df
self.prices: DataFrame = prices
self.window_size: int = window_size
self.starting_point: bool = starting_point
self.rr: float = reward_kwargs["rr"]
self.profit_aim: float = reward_kwargs["profit_aim"]
# # spaces
if self.add_state_info:

View File

@@ -1,3 +1,4 @@
import copy
import importlib
import logging
from abc import abstractmethod
@@ -50,6 +51,7 @@ class BaseReinforcementLearningModel(IFreqaiModel):
self.eval_callback: Optional[EvalCallback] = None
self.model_type = self.freqai_info['rl_config']['model_type']
self.rl_config = self.freqai_info['rl_config']
self.df_raw: DataFrame = DataFrame()
self.continual_learning = self.freqai_info.get('continual_learning', False)
if self.model_type in SB3_MODELS:
import_str = 'stable_baselines3'
@@ -107,6 +109,7 @@ class BaseReinforcementLearningModel(IFreqaiModel):
data_dictionary: Dict[str, Any] = dk.make_train_test_datasets(
features_filtered, labels_filtered)
self.df_raw = copy.deepcopy(data_dictionary["train_features"])
dk.fit_labels() # FIXME useless for now, but just satiating append methods
# normalize all data based on train_dataset only
@@ -143,7 +146,7 @@ class BaseReinforcementLearningModel(IFreqaiModel):
train_df = data_dictionary["train_features"]
test_df = data_dictionary["test_features"]
env_info = self.pack_env_dict()
env_info = self.pack_env_dict(dk.pair)
self.train_env = self.MyRLEnv(df=train_df,
prices=prices_train,
@@ -158,14 +161,17 @@ class BaseReinforcementLearningModel(IFreqaiModel):
actions = self.train_env.get_actions()
self.tensorboard_callback = TensorboardCallback(verbose=1, actions=actions)
def pack_env_dict(self) -> Dict[str, Any]:
def pack_env_dict(self, pair: str) -> Dict[str, Any]:
"""
Create dictionary of environment arguments
"""
env_info = {"window_size": self.CONV_WIDTH,
"reward_kwargs": self.reward_params,
"config": self.config,
"live": self.live}
"live": self.live,
"can_short": self.can_short,
"pair": pair,
"df_raw": self.df_raw}
if self.data_provider:
env_info["fee"] = self.data_provider._exchange \
.get_fee(symbol=self.data_provider.current_whitelist()[0]) # type: ignore
@@ -279,26 +285,36 @@ class BaseReinforcementLearningModel(IFreqaiModel):
train_df = data_dictionary["train_features"]
test_df = data_dictionary["test_features"]
# %-raw_volume_gen_shift-2_ETH/USDT_1h
# price data for model training and evaluation
tf = self.config['timeframe']
ohlc_list = [f'%-{pair}raw_open_{tf}', f'%-{pair}raw_low_{tf}',
f'%-{pair}raw_high_{tf}', f'%-{pair}raw_close_{tf}']
rename_dict = {f'%-{pair}raw_open_{tf}': 'open', f'%-{pair}raw_low_{tf}': 'low',
f'%-{pair}raw_high_{tf}': ' high', f'%-{pair}raw_close_{tf}': 'close'}
rename_dict = {'%-raw_open': 'open', '%-raw_low': 'low',
'%-raw_high': ' high', '%-raw_close': 'close'}
rename_dict_old = {f'%-{pair}raw_open_{tf}': 'open', f'%-{pair}raw_low_{tf}': 'low',
f'%-{pair}raw_high_{tf}': ' high', f'%-{pair}raw_close_{tf}': 'close'}
prices_train = train_df.filter(rename_dict.keys(), axis=1)
prices_train_old = train_df.filter(rename_dict_old.keys(), axis=1)
if prices_train.empty or not prices_train_old.empty:
if not prices_train_old.empty:
prices_train = prices_train_old
rename_dict = rename_dict_old
logger.warning('Reinforcement learning module didnt find the correct raw prices '
'assigned in feature_engineering_standard(). '
'Please assign them with:\n'
'dataframe["%-raw_close"] = dataframe["close"]\n'
'dataframe["%-raw_open"] = dataframe["open"]\n'
'dataframe["%-raw_high"] = dataframe["high"]\n'
'dataframe["%-raw_low"] = dataframe["low"]\n'
'inside `feature_engineering_standard()')
elif prices_train.empty:
raise OperationalException("No prices found, please follow log warning "
"instructions to correct the strategy.")
prices_train = train_df.filter(ohlc_list, axis=1)
if prices_train.empty:
raise OperationalException('Reinforcement learning module didnt find the raw prices '
'assigned in populate_any_indicators. Please assign them '
'with:\n'
'informative[f"%-{pair}raw_close"] = informative["close"]\n'
'informative[f"%-{pair}raw_open"] = informative["open"]\n'
'informative[f"%-{pair}raw_high"] = informative["high"]\n'
'informative[f"%-{pair}raw_low"] = informative["low"]\n')
prices_train.rename(columns=rename_dict, inplace=True)
prices_train.reset_index(drop=True)
prices_test = test_df.filter(ohlc_list, axis=1)
prices_test = test_df.filter(rename_dict.keys(), axis=1)
prices_test.rename(columns=rename_dict, inplace=True)
prices_test.reset_index(drop=True)
@@ -336,7 +352,7 @@ class BaseReinforcementLearningModel(IFreqaiModel):
sets a custom reward based on profit and trade duration.
"""
def calculate_reward(self, action: int) -> float:
def calculate_reward(self, action: int) -> float: # noqa: C901
"""
An example reward function. This is the one function that users will likely
wish to inject their own creativity into.
@@ -352,10 +368,19 @@ class BaseReinforcementLearningModel(IFreqaiModel):
pnl = self.get_unrealized_profit()
factor = 100.
# you can use feature values from dataframe
rsi_now = self.raw_features[f"%-rsi-period-10_shift-1_{self.pair}_"
f"{self.config['timeframe']}"].iloc[self._current_tick]
# reward agent for entering trades
if (action in (Actions.Long_enter.value, Actions.Short_enter.value)
and self._position == Positions.Neutral):
return 25
if rsi_now < 40:
factor = 40 / rsi_now
else:
factor = 1
return 25 * factor
# discourage agent from not entering trades
if action == Actions.Neutral.value and self._position == Positions.Neutral:
return -1

View File

@@ -59,7 +59,7 @@ class FreqaiDataDrawer:
Juha Nykänen @suikula, Wagner Costa @wagnercosta, Johan Vlugt @Jooopieeert
"""
def __init__(self, full_path: Path, config: Config, follow_mode: bool = False):
def __init__(self, full_path: Path, config: Config):
self.config = config
self.freqai_info = config.get("freqai", {})
@@ -84,9 +84,6 @@ class FreqaiDataDrawer:
self.pair_dictionary_path = Path(self.full_path / "pair_dictionary.json")
self.global_metadata_path = Path(self.full_path / "global_metadata.json")
self.metric_tracker_path = Path(self.full_path / "metric_tracker.json")
self.follow_mode = follow_mode
if follow_mode:
self.create_follower_dict()
self.load_drawer_from_disk()
self.load_historic_predictions_from_disk()
self.metric_tracker: Dict[str, Dict[str, Dict[str, list]]] = {}
@@ -149,13 +146,8 @@ class FreqaiDataDrawer:
if exists:
with open(self.pair_dictionary_path, "r") as fp:
self.pair_dict = rapidjson.load(fp, number_mode=rapidjson.NM_NATIVE)
elif not self.follow_mode:
logger.info("Could not find existing datadrawer, starting from scratch")
else:
logger.warning(
f"Follower could not find pair_dictionary at {self.full_path} "
"sending null values back to strategy"
)
logger.info("Could not find existing datadrawer, starting from scratch")
def load_metric_tracker_from_disk(self):
"""
@@ -193,13 +185,8 @@ class FreqaiDataDrawer:
self.historic_predictions = cloudpickle.load(fp)
logger.warning('FreqAI successfully loaded the backup historical predictions file.')
elif not self.follow_mode:
logger.info("Could not find existing historic_predictions, starting from scratch")
else:
logger.warning(
f"Follower could not find historic predictions at {self.full_path} "
"sending null values back to strategy"
)
logger.info("Could not find existing historic_predictions, starting from scratch")
return exists
@@ -248,23 +235,6 @@ class FreqaiDataDrawer:
rapidjson.dump(metadata, fp, default=self.np_encoder,
number_mode=rapidjson.NM_NATIVE)
def create_follower_dict(self):
"""
Create or dictionary for each follower to maintain unique persistent prediction targets
"""
whitelist_pairs = self.config.get("exchange", {}).get("pair_whitelist")
exists = self.follower_dict_path.is_file()
if exists:
logger.info("Found an existing follower dictionary")
for pair in whitelist_pairs:
self.follower_dict[pair] = {}
self.save_follower_dict_to_disk()
def np_encoder(self, object):
if isinstance(object, np.generic):
return object.item()
@@ -282,27 +252,17 @@ class FreqaiDataDrawer:
"""
pair_dict = self.pair_dict.get(pair)
data_path_set = self.pair_dict.get(pair, self.empty_pair_dict).get("data_path", "")
# data_path_set = self.pair_dict.get(pair, self.empty_pair_dict).get("data_path", "")
return_null_array = False
if pair_dict:
model_filename = pair_dict["model_filename"]
trained_timestamp = pair_dict["trained_timestamp"]
elif not self.follow_mode:
else:
self.pair_dict[pair] = self.empty_pair_dict.copy()
model_filename = ""
trained_timestamp = 0
if not data_path_set and self.follow_mode:
logger.warning(
f"Follower could not find current pair {pair} in "
f"pair_dictionary at path {self.full_path}, sending null values "
"back to strategy."
)
trained_timestamp = 0
model_filename = ''
return_null_array = True
return model_filename, trained_timestamp, return_null_array
def set_pair_dict_info(self, metadata: dict) -> None:
@@ -311,7 +271,6 @@ class FreqaiDataDrawer:
return
else:
self.pair_dict[metadata["pair"]] = self.empty_pair_dict.copy()
return
def set_initial_return_values(self, pair: str, pred_df: DataFrame) -> None:

View File

@@ -1,11 +1,12 @@
import copy
import inspect
import logging
import random
import shutil
from datetime import datetime, timezone
from math import cos, sin
from pathlib import Path
from typing import Any, Dict, List, Tuple
from typing import Any, Dict, List, Optional, Tuple
import numpy as np
import numpy.typing as npt
@@ -24,6 +25,7 @@ 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 import merge_informative_pair
from freqtrade.strategy.interface import IStrategy
@@ -111,7 +113,7 @@ class FreqaiDataKitchen:
def set_paths(
self,
pair: str,
trained_timestamp: int = None,
trained_timestamp: Optional[int] = None,
) -> None:
"""
Set the paths to the data for the present coin/botloop
@@ -1159,9 +1161,9 @@ class FreqaiDataKitchen:
for pair in pairs:
pair = pair.replace(':', '') # lightgbm doesnt like colons
valid_strs = [f"%-{pair}", f"%{pair}", f"%_{pair}"]
pair_cols = [col for col in dataframe.columns if
any(substr in col for substr in valid_strs)]
pair_cols = [col for col in dataframe.columns if col.startswith("%")
and f"{pair}_" in col]
if pair_cols:
pair_cols.insert(0, 'date')
corr_dataframes[pair] = dataframe.filter(pair_cols, axis=1)
@@ -1190,6 +1192,105 @@ class FreqaiDataKitchen:
return dataframe
def get_pair_data_for_features(self,
pair: str,
tf: str,
strategy: IStrategy,
corr_dataframes: dict = {},
base_dataframes: dict = {},
is_corr_pairs: bool = False) -> DataFrame:
"""
Get the data for the pair. If it's not in the dictionary, get it from the data provider
:param pair: str = pair to get data for
:param tf: str = timeframe to get data for
:param strategy: IStrategy = user defined strategy object
:param corr_dataframes: dict = dict containing the df pair dataframes
(for user defined timeframes)
:param base_dataframes: dict = dict containing the current pair dataframes
(for user defined timeframes)
:param is_corr_pairs: bool = whether the pair is a corr pair or not
:return: dataframe = dataframe containing the pair data
"""
if is_corr_pairs:
dataframe = corr_dataframes[pair][tf]
if not dataframe.empty:
return dataframe
else:
dataframe = strategy.dp.get_pair_dataframe(pair=pair, timeframe=tf)
return dataframe
else:
dataframe = base_dataframes[tf]
if not dataframe.empty:
return dataframe
else:
dataframe = strategy.dp.get_pair_dataframe(pair=pair, timeframe=tf)
return dataframe
def merge_features(self, df_main: DataFrame, df_to_merge: DataFrame,
tf: str, timeframe_inf: str, suffix: str) -> DataFrame:
"""
Merge the features of the dataframe and remove HLCV and date added columns
:param df_main: DataFrame = main dataframe
:param df_to_merge: DataFrame = dataframe to merge
:param tf: str = timeframe of the main dataframe
:param timeframe_inf: str = timeframe of the dataframe to merge
:param suffix: str = suffix to add to the columns of the dataframe to merge
:return: dataframe = merged dataframe
"""
dataframe = merge_informative_pair(df_main, df_to_merge, tf, timeframe_inf=timeframe_inf,
append_timeframe=False, suffix=suffix, ffill=True)
skip_columns = [
(f"{s}_{suffix}") for s in ["date", "open", "high", "low", "close", "volume"]
]
dataframe = dataframe.drop(columns=skip_columns)
return dataframe
def populate_features(self, dataframe: DataFrame, pair: str, strategy: IStrategy,
corr_dataframes: dict, base_dataframes: dict,
is_corr_pairs: bool = False) -> DataFrame:
"""
Use the user defined strategy functions for populating features
:param dataframe: DataFrame = dataframe to populate
:param pair: str = pair to populate
:param strategy: IStrategy = user defined strategy object
:param corr_dataframes: dict = dict containing the df pair dataframes
:param base_dataframes: dict = dict containing the current pair dataframes
:param is_corr_pairs: bool = whether the pair is a corr pair or not
:return: dataframe = populated dataframe
"""
tfs: List[str] = self.freqai_config["feature_parameters"].get("include_timeframes")
for tf in tfs:
metadata = {"pair": pair, "tf": tf}
informative_df = self.get_pair_data_for_features(
pair, tf, strategy, corr_dataframes, base_dataframes, is_corr_pairs)
informative_copy = informative_df.copy()
for t in self.freqai_config["feature_parameters"]["indicator_periods_candles"]:
df_features = strategy.feature_engineering_expand_all(
informative_copy.copy(), t, metadata=metadata)
suffix = f"{t}"
informative_df = self.merge_features(informative_df, df_features, tf, tf, suffix)
generic_df = strategy.feature_engineering_expand_basic(
informative_copy.copy(), metadata=metadata)
suffix = "gen"
informative_df = self.merge_features(informative_df, generic_df, tf, tf, suffix)
indicators = [col for col in informative_df if col.startswith("%")]
for n in range(self.freqai_config["feature_parameters"]["include_shifted_candles"] + 1):
if n == 0:
continue
df_shift = informative_df[indicators].shift(n)
df_shift = df_shift.add_suffix("_shift-" + str(n))
informative_df = pd.concat((informative_df, df_shift), axis=1)
dataframe = self.merge_features(dataframe.copy(), informative_df,
self.config["timeframe"], tf, f'{pair}_{tf}')
return dataframe
def use_strategy_to_populate_indicators(
self,
strategy: IStrategy,
@@ -1202,7 +1303,87 @@ class FreqaiDataKitchen:
"""
Use the user defined strategy for populating indicators during retrain
:param strategy: IStrategy = user defined strategy object
:param corr_dataframes: dict = dict containing the informative pair dataframes
:param corr_dataframes: dict = dict containing the df pair dataframes
(for user defined timeframes)
:param base_dataframes: dict = dict containing the current pair dataframes
(for user defined timeframes)
:param pair: str = pair to populate
:param prediction_dataframe: DataFrame = dataframe containing the pair data
used for prediction
:param do_corr_pairs: bool = whether to populate corr pairs or not
:return:
dataframe: DataFrame = dataframe containing populated indicators
"""
# this is a hack to check if the user is using the populate_any_indicators function
new_version = inspect.getsource(strategy.populate_any_indicators) == (
inspect.getsource(IStrategy.populate_any_indicators))
if new_version:
tfs: List[str] = self.freqai_config["feature_parameters"].get("include_timeframes")
pairs: List[str] = self.freqai_config["feature_parameters"].get(
"include_corr_pairlist", [])
for tf in tfs:
if tf not in base_dataframes:
base_dataframes[tf] = pd.DataFrame()
for p in pairs:
if p not in corr_dataframes:
corr_dataframes[p] = {}
if tf not in corr_dataframes[p]:
corr_dataframes[p][tf] = pd.DataFrame()
if not prediction_dataframe.empty:
dataframe = prediction_dataframe.copy()
else:
dataframe = base_dataframes[self.config["timeframe"]].copy()
corr_pairs: List[str] = self.freqai_config["feature_parameters"].get(
"include_corr_pairlist", [])
dataframe = self.populate_features(dataframe.copy(), pair, strategy,
corr_dataframes, base_dataframes)
metadata = {"pair": pair}
dataframe = strategy.feature_engineering_standard(dataframe.copy(), metadata=metadata)
# ensure corr pairs are always last
for corr_pair in corr_pairs:
if pair == corr_pair:
continue # dont repeat anything from whitelist
if corr_pairs and do_corr_pairs:
dataframe = self.populate_features(dataframe.copy(), corr_pair, strategy,
corr_dataframes, base_dataframes, True)
dataframe = strategy.set_freqai_targets(dataframe.copy(), metadata=metadata)
self.get_unique_classes_from_labels(dataframe)
dataframe = self.remove_special_chars_from_feature_names(dataframe)
if self.config.get('reduce_df_footprint', False):
dataframe = reduce_dataframe_footprint(dataframe)
return dataframe
else:
# the user is using the populate_any_indicators functions which is deprecated
df = self.use_strategy_to_populate_indicators_old_version(
strategy, corr_dataframes, base_dataframes, pair,
prediction_dataframe, do_corr_pairs)
return df
def use_strategy_to_populate_indicators_old_version(
self,
strategy: IStrategy,
corr_dataframes: dict = {},
base_dataframes: dict = {},
pair: str = "",
prediction_dataframe: DataFrame = pd.DataFrame(),
do_corr_pairs: bool = True,
) -> DataFrame:
"""
Use the user defined strategy for populating indicators during retrain
:param strategy: IStrategy = user defined strategy object
:param corr_dataframes: dict = dict containing the df pair dataframes
(for user defined timeframes)
:param base_dataframes: dict = dict containing the current pair dataframes
(for user defined timeframes)

View File

@@ -1,3 +1,4 @@
import inspect
import logging
import threading
import time
@@ -65,12 +66,11 @@ class IFreqaiModel(ABC):
self.retrain = False
self.first = True
self.set_full_path()
self.follow_mode: bool = self.freqai_info.get("follow_mode", False)
self.save_backtest_models: bool = self.freqai_info.get("save_backtest_models", True)
if self.save_backtest_models:
logger.info('Backtesting module configured to save all models.')
self.dd = FreqaiDataDrawer(Path(self.full_path), self.config, self.follow_mode)
self.dd = FreqaiDataDrawer(Path(self.full_path), self.config)
# set current candle to arbitrary historical date
self.current_candle: datetime = datetime.fromtimestamp(637887600, tz=timezone.utc)
self.dd.current_candle = self.current_candle
@@ -104,6 +104,9 @@ class IFreqaiModel(ABC):
self.metadata: Dict[str, Any] = self.dd.load_global_metadata_from_disk()
self.data_provider: Optional[DataProvider] = None
self.max_system_threads = max(int(psutil.cpu_count() * 2 - 2), 1)
self.can_short = True # overridden in start() with strategy.can_short
self.warned_deprecated_populate_any_indicators = False
record_params(config, self.full_path)
@@ -133,6 +136,10 @@ class IFreqaiModel(ABC):
self.live = strategy.dp.runmode in (RunMode.DRY_RUN, RunMode.LIVE)
self.dd.set_pair_dict_info(metadata)
self.data_provider = strategy.dp
self.can_short = strategy.can_short
# check if the strategy has deprecated populate_any_indicators function
self.check_deprecated_populate_any_indicators(strategy)
if self.live:
self.inference_timer('start')
@@ -145,14 +152,11 @@ class IFreqaiModel(ABC):
# (backtest window, i.e. window immediately following the training window).
# FreqAI slides the window and sequentially builds the backtesting results before returning
# the concatenated results for the full backtesting period back to the strategy.
elif not self.follow_mode:
else:
self.dk = FreqaiDataKitchen(self.config, self.live, metadata["pair"])
dataframe = self.dk.use_strategy_to_populate_indicators(
strategy, prediction_dataframe=dataframe, pair=metadata["pair"]
)
if not self.config.get("freqai_backtest_live_models", False):
logger.info(f"Training {len(self.dk.training_timeranges)} timeranges")
dk = self.start_backtesting(dataframe, metadata, self.dk)
dk = self.start_backtesting(dataframe, metadata, self.dk, strategy)
dataframe = dk.remove_features_from_df(dk.return_dataframe)
else:
logger.info(
@@ -253,7 +257,7 @@ class IFreqaiModel(ABC):
self.dd.save_metric_tracker_to_disk()
def start_backtesting(
self, dataframe: DataFrame, metadata: dict, dk: FreqaiDataKitchen
self, dataframe: DataFrame, metadata: dict, dk: FreqaiDataKitchen, strategy: IStrategy
) -> FreqaiDataKitchen:
"""
The main broad execution for backtesting. For backtesting, each pair enters and then gets
@@ -265,19 +269,22 @@ class IFreqaiModel(ABC):
:param dataframe: DataFrame = strategy passed dataframe
:param metadata: Dict = pair metadata
:param dk: FreqaiDataKitchen = Data management/analysis tool associated to present pair only
:param strategy: Strategy to train on
:return:
FreqaiDataKitchen = Data management/analysis tool associated to present pair only
"""
self.pair_it += 1
train_it = 0
pair = metadata["pair"]
populate_indicators = True
check_features = True
# Loop enforcing the sliding window training/backtesting paradigm
# tr_train is the training time range e.g. 1 historical month
# tr_backtest is the backtesting time range e.g. the week directly
# following tr_train. Both of these windows slide through the
# entire backtest
for tr_train, tr_backtest in zip(dk.training_timeranges, dk.backtesting_timeranges):
pair = metadata["pair"]
(_, _, _) = self.dd.get_pair_dict_info(pair)
train_it += 1
total_trains = len(dk.backtesting_timeranges)
@@ -299,18 +306,44 @@ class IFreqaiModel(ABC):
dk.set_new_model_names(pair, timestamp_model_id)
if dk.check_if_backtest_prediction_is_valid(len_backtest_df):
self.dd.load_metadata(dk)
dk.find_features(dataframe)
self.check_if_feature_list_matches_strategy(dk)
if check_features:
self.dd.load_metadata(dk)
dataframe_dummy_features = self.dk.use_strategy_to_populate_indicators(
strategy, prediction_dataframe=dataframe.tail(1), pair=metadata["pair"]
)
dk.find_features(dataframe_dummy_features)
self.check_if_feature_list_matches_strategy(dk)
check_features = False
append_df = dk.get_backtesting_prediction()
dk.append_predictions(append_df)
else:
dataframe_train = dk.slice_dataframe(tr_train, dataframe)
dataframe_backtest = dk.slice_dataframe(tr_backtest, dataframe)
if populate_indicators:
dataframe = self.dk.use_strategy_to_populate_indicators(
strategy, prediction_dataframe=dataframe, pair=metadata["pair"]
)
populate_indicators = False
dataframe_base_train = dataframe.loc[dataframe["date"] < tr_train.stopdt, :]
dataframe_base_train = strategy.set_freqai_targets(
dataframe_base_train, metadata=metadata)
dataframe_base_backtest = dataframe.loc[dataframe["date"] < tr_backtest.stopdt, :]
dataframe_base_backtest = strategy.set_freqai_targets(
dataframe_base_backtest, metadata=metadata)
dataframe_train = dk.slice_dataframe(tr_train, dataframe_base_train)
dataframe_backtest = dk.slice_dataframe(tr_backtest, dataframe_base_backtest)
if not self.model_exists(dk):
dk.find_features(dataframe_train)
dk.find_labels(dataframe_train)
self.model = self.train(dataframe_train, pair, dk)
try:
self.model = self.train(dataframe_train, pair, dk)
except Exception as msg:
logger.warning(
f"Training {pair} raised exception {msg.__class__.__name__}. "
f"Message: {msg}, skipping.")
self.dd.pair_dict[pair]["trained_timestamp"] = int(
tr_train.stopts)
if self.plot_features:
@@ -348,46 +381,27 @@ class IFreqaiModel(ABC):
dk: FreqaiDataKitchen = Data management/analysis tool associated to present pair only
"""
# update follower
if self.follow_mode:
self.dd.update_follower_metadata()
# get the model metadata associated with the current pair
(_, trained_timestamp, return_null_array) = self.dd.get_pair_dict_info(metadata["pair"])
# if the metadata doesn't exist, the follower returns null arrays to strategy
if self.follow_mode and return_null_array:
logger.info("Returning null array from follower to strategy")
self.dd.return_null_values_to_strategy(dataframe, dk)
return dk
# append the historic data once per round
if self.dd.historic_data:
self.dd.update_historic_data(strategy, dk)
logger.debug(f'Updating historic data on pair {metadata["pair"]}')
self.track_current_candle()
if not self.follow_mode:
(_, new_trained_timerange, data_load_timerange) = dk.check_if_new_training_required(
trained_timestamp
)
dk.set_paths(metadata["pair"], new_trained_timerange.stopts)
(_, new_trained_timerange, data_load_timerange) = dk.check_if_new_training_required(
trained_timestamp
)
dk.set_paths(metadata["pair"], new_trained_timerange.stopts)
# load candle history into memory if it is not yet.
if not self.dd.historic_data:
self.dd.load_all_pair_histories(data_load_timerange, dk)
# load candle history into memory if it is not yet.
if not self.dd.historic_data:
self.dd.load_all_pair_histories(data_load_timerange, dk)
if not self.scanning:
self.scanning = True
self.start_scanning(strategy)
elif self.follow_mode:
dk.set_paths(metadata["pair"], trained_timestamp)
logger.info(
"FreqAI instance set to follow_mode, finding existing pair "
f"using { self.identifier }"
)
if not self.scanning:
self.scanning = True
self.start_scanning(strategy)
# load the model and associated data into the data kitchen
self.model = self.dd.load_data(metadata["pair"], dk)
@@ -911,9 +925,28 @@ class IFreqaiModel(ABC):
dk.return_dataframe = dk.return_dataframe.drop(columns=list(columns_to_drop))
dk.return_dataframe = pd.merge(
dk.return_dataframe, saved_dataframe, how='left', left_on='date', right_on="date_pred")
# dk.return_dataframe = dk.return_dataframe[saved_dataframe.columns].fillna(0)
return dk
def check_deprecated_populate_any_indicators(self, strategy: IStrategy):
"""
Check and warn if the deprecated populate_any_indicators function is used.
:param strategy: strategy object
"""
if not self.warned_deprecated_populate_any_indicators:
self.warned_deprecated_populate_any_indicators = True
old_version = inspect.getsource(strategy.populate_any_indicators) != (
inspect.getsource(IStrategy.populate_any_indicators))
if old_version:
logger.warning("DEPRECATION WARNING: "
"You are using the deprecated populate_any_indicators function. "
"This function will raise an error on March 1 2023. "
"Please update your strategy by using "
"the new feature_engineering functions. See \n"
"https://www.freqtrade.io/en/latest/freqai-feature-engineering/"
"for details.")
# Following methods which are overridden by user made prediction models.
# See freqai/prediction_models/CatboostPredictionModel.py for an example.

View File

@@ -34,7 +34,7 @@ class ReinforcementLearner_multiproc(ReinforcementLearner):
train_df = data_dictionary["train_features"]
test_df = data_dictionary["test_features"]
env_info = self.pack_env_dict()
env_info = self.pack_env_dict(dk.pair)
env_id = "train_env"
self.train_env = SubprocVecEnv([make_env(self.MyRLEnv, env_id, i, 1,

View File

@@ -33,6 +33,7 @@ from freqtrade.rpc.external_message_consumer import ExternalMessageConsumer
from freqtrade.strategy.interface import IStrategy
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
from freqtrade.util import FtPrecise
from freqtrade.util.binance_mig import migrate_binance_futures_names
from freqtrade.wallets import Wallets
@@ -177,6 +178,8 @@ class FreqtradeBot(LoggingMixin):
Called on startup and after reloading the bot - triggers notifications and
performs startup tasks
"""
migrate_binance_futures_names(self.config)
self.rpc.startup_messages(self.config, self.pairlists, self.protections)
# Update older trades with precision and precision mode
self.startup_backpopulate_precision()
@@ -341,7 +344,15 @@ class FreqtradeBot(LoggingMixin):
try:
fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair,
order.ft_order_side == 'stoploss')
if not order.trade:
# This should not happen, but it does if trades were deleted manually.
# This can only incur on sqlite, which doesn't enforce foreign constraints.
logger.warning(
f"Order {order.order_id} has no trade attached. "
"This may suggest a database corruption. "
f"The expected trade ID is {order.ft_trade_id}. Ignoring this order."
)
continue
self.update_trade_state(order.trade, order.order_id, fo,
stoploss_order=(order.ft_order_side == 'stoploss'))
@@ -352,7 +363,7 @@ class FreqtradeBot(LoggingMixin):
"Order is older than 5 days. Assuming order was fully cancelled.")
fo = order.to_ccxt_object()
fo['status'] = 'canceled'
self.handle_timedout_order(fo, order.trade)
self.handle_cancel_order(fo, order.trade, constants.CANCEL_REASON['TIMEOUT'])
except ExchangeError as e:
@@ -374,7 +385,7 @@ class FreqtradeBot(LoggingMixin):
for trade in trades:
if not trade.is_open and not trade.fee_updated(trade.exit_side):
# Get sell fee
order = trade.select_order(trade.exit_side, False)
order = trade.select_order(trade.exit_side, False, only_filled=True)
if not order:
order = trade.select_order('stoploss', False)
if order:
@@ -390,7 +401,7 @@ class FreqtradeBot(LoggingMixin):
for trade in trades:
with self._exit_lock:
if trade.is_open and not trade.fee_updated(trade.entry_side):
order = trade.select_order(trade.entry_side, False)
order = trade.select_order(trade.entry_side, False, only_filled=True)
open_order = trade.select_order(trade.entry_side, True)
if order and open_order is None:
logger.info(
@@ -720,7 +731,7 @@ class FreqtradeBot(LoggingMixin):
time_in_force=time_in_force,
leverage=leverage
)
order_obj = Order.parse_from_ccxt_object(order, pair, side)
order_obj = Order.parse_from_ccxt_object(order, pair, side, amount, enter_limit_requested)
order_id = order['id']
order_status = order.get('status')
logger.info(f"Order #{order_id} was created for {pair} and status is {order_status}.")
@@ -747,13 +758,15 @@ class FreqtradeBot(LoggingMixin):
self.exchange.name, order['filled'], order['amount'],
order['remaining']
)
amount = safe_value_fallback(order, 'filled', 'amount')
enter_limit_filled_price = safe_value_fallback(order, 'average', 'price')
amount = safe_value_fallback(order, 'filled', 'amount', amount)
enter_limit_filled_price = safe_value_fallback(
order, 'average', 'price', enter_limit_filled_price)
# in case of FOK the order may be filled immediately and fully
elif order_status == 'closed':
amount = safe_value_fallback(order, 'filled', 'amount')
enter_limit_filled_price = safe_value_fallback(order, 'average', 'price')
amount = safe_value_fallback(order, 'filled', 'amount', amount)
enter_limit_filled_price = safe_value_fallback(
order, 'average', 'price', enter_limit_requested)
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker')
@@ -912,6 +925,7 @@ class FreqtradeBot(LoggingMixin):
stake_amount=stake_amount,
min_stake_amount=min_stake_amount,
max_stake_amount=max_stake_amount,
trade_amount=trade.stake_amount if trade else None,
)
return enter_limit_requested, stake_amount, leverage
@@ -1064,7 +1078,7 @@ class FreqtradeBot(LoggingMixin):
datetime.now(timezone.utc),
enter=enter,
exit_=exit_,
force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0
force_stoploss=self.edge.get_stoploss(trade.pair) if self.edge else 0
)
for should_exit in exits:
if should_exit.exit_flag:
@@ -1084,7 +1098,7 @@ class FreqtradeBot(LoggingMixin):
:return: True if the order succeeded, and False in case of problems.
"""
try:
stoploss_order = self.exchange.stoploss(
stoploss_order = self.exchange.create_stoploss(
pair=trade.pair,
amount=trade.amount,
stop_price=stop_price,
@@ -1093,7 +1107,8 @@ class FreqtradeBot(LoggingMixin):
leverage=trade.leverage
)
order_obj = Order.parse_from_ccxt_object(stoploss_order, trade.pair, 'stoploss')
order_obj = Order.parse_from_ccxt_object(stoploss_order, trade.pair, 'stoploss',
trade.amount, stop_price)
trade.orders.append(order_obj)
trade.stoploss_order_id = str(stoploss_order['id'])
trade.stoploss_last_update = datetime.now(timezone.utc)
@@ -1155,15 +1170,13 @@ class FreqtradeBot(LoggingMixin):
# If enter order is fulfilled but there is no stoploss, we add a stoploss on exchange
if not stoploss_order:
stoploss = (
self.edge.stoploss(pair=trade.pair)
if self.edge else
trade.stop_loss_pct / trade.leverage
)
if trade.is_short:
stop_price = trade.open_rate * (1 - stoploss)
else:
stop_price = trade.open_rate * (1 + stoploss)
stop_price = trade.stoploss_or_liquidation
if self.edge:
stoploss = self.edge.get_stoploss(pair=trade.pair)
stop_price = (
trade.open_rate * (1 - stoploss) if trade.is_short
else trade.open_rate * (1 + stoploss)
)
if self.create_stoploss_order(trade=trade, stop_price=stop_price):
# The above will return False if the placement failed and the trade was force-sold.
@@ -1248,11 +1261,11 @@ class FreqtradeBot(LoggingMixin):
if not_closed:
if fully_cancelled or (order_obj and self.strategy.ft_check_timed_out(
trade, order_obj, datetime.now(timezone.utc))):
self.handle_timedout_order(order, trade)
self.handle_cancel_order(order, trade, constants.CANCEL_REASON['TIMEOUT'])
else:
self.replace_order(order, order_obj, trade)
def handle_timedout_order(self, order: Dict, trade: Trade) -> None:
def handle_cancel_order(self, order: Dict, trade: Trade, reason: str) -> None:
"""
Check if current analyzed order timed out and cancel if necessary.
:param order: Order dict grabbed with exchange.fetch_order()
@@ -1260,10 +1273,10 @@ class FreqtradeBot(LoggingMixin):
:return: None
"""
if order['side'] == trade.entry_side:
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT'])
self.handle_cancel_enter(trade, order, reason)
else:
canceled = self.handle_cancel_exit(
trade, order, constants.CANCEL_REASON['TIMEOUT'])
trade, order, reason)
canceled_count = trade.get_exit_order_count()
max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0)
if canceled and max_timeouts > 0 and canceled_count >= max_timeouts:
@@ -1517,7 +1530,7 @@ class FreqtradeBot(LoggingMixin):
*,
exit_tag: Optional[str] = None,
ordertype: Optional[str] = None,
sub_trade_amt: float = None,
sub_trade_amt: Optional[float] = None,
) -> bool:
"""
Executes a trade exit for the given trade and limit
@@ -1594,7 +1607,7 @@ class FreqtradeBot(LoggingMixin):
self.handle_insufficient_funds(trade)
return False
order_obj = Order.parse_from_ccxt_object(order, trade.pair, trade.exit_side)
order_obj = Order.parse_from_ccxt_object(order, trade.pair, trade.exit_side, amount, limit)
trade.orders.append(order_obj)
trade.open_order_id = order['id']
@@ -1611,7 +1624,7 @@ class FreqtradeBot(LoggingMixin):
return True
def _notify_exit(self, trade: Trade, order_type: str, fill: bool = False,
sub_trade: bool = False, order: Order = None) -> None:
sub_trade: bool = False, order: Optional[Order] = None) -> None:
"""
Sends rpc notification when a sell occurred.
"""
@@ -1621,7 +1634,7 @@ class FreqtradeBot(LoggingMixin):
# second condition is for mypy only; order will always be passed during sub trade
if sub_trade and order is not None:
amount = order.safe_filled if fill else order.amount
amount = order.safe_filled if fill else order.safe_amount
order_rate: float = order.safe_price
profit = trade.calc_profit(rate=order_rate, amount=amount, open_rate=trade.open_rate)
@@ -1724,8 +1737,9 @@ class FreqtradeBot(LoggingMixin):
# Common update trade state methods
#
def update_trade_state(self, trade: Trade, order_id: str, action_order: Dict[str, Any] = None,
stoploss_order: bool = False, send_msg: bool = True) -> bool:
def update_trade_state(
self, trade: Trade, order_id: str, action_order: Optional[Dict[str, Any]] = None,
stoploss_order: bool = False, send_msg: bool = True) -> bool:
"""
Checks trades with open orders and updates the amount if necessary
Handles closing both buy and sell orders.
@@ -1783,6 +1797,7 @@ class FreqtradeBot(LoggingMixin):
is_short=trade.is_short,
amount=trade.amount,
stake_amount=trade.stake_amount,
leverage=trade.leverage,
wallet_balance=trade.stake_amount,
))

View File

@@ -5,7 +5,7 @@ Read the documentation to know what cli arguments you need.
"""
import logging
import sys
from typing import Any, List
from typing import Any, List, Optional
from freqtrade.util.gc_setup import gc_set_threshold
@@ -23,7 +23,7 @@ from freqtrade.loggers import setup_logging_pre
logger = logging.getLogger('freqtrade')
def main(sysargv: List[str] = None) -> None:
def main(sysargv: Optional[List[str]] = None) -> None:
"""
This function will initiate the bot and start the trading loop.
:return: None

View File

@@ -6,7 +6,7 @@ import logging
import re
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, Iterator, List, Mapping, Union
from typing import Any, Dict, Iterator, List, Mapping, Optional, Union
from typing.io import IO
from urllib.parse import urlparse
@@ -205,7 +205,7 @@ def safe_value_fallback2(dict1: dictMap, dict2: dictMap, key1: str, key2: str, d
return default_value
def plural(num: float, singular: str, plural: str = None) -> str:
def plural(num: float, singular: str, plural: Optional[str] = None) -> str:
return singular if (num == 1 or num == -1) else plural or singular + 's'
@@ -269,6 +269,8 @@ def dataframe_to_json(dataframe: pd.DataFrame) -> str:
def default(z):
if isinstance(z, pd.Timestamp):
return z.timestamp() * 1e3
if z is pd.NaT:
return 'NaT'
raise TypeError
return str(orjson.dumps(dataframe.to_dict(orient='split'), default=default), 'utf-8')
@@ -301,3 +303,21 @@ def remove_entry_exit_signals(dataframe: pd.DataFrame):
dataframe[SignalTagType.EXIT_TAG.value] = None
return dataframe
def append_candles_to_dataframe(left: pd.DataFrame, right: pd.DataFrame) -> pd.DataFrame:
"""
Append the `right` dataframe to the `left` dataframe
:param left: The full dataframe you want appended to
:param right: The new dataframe containing the data you want appended
:returns: The dataframe with the right data in it
"""
if left.iloc[-1]['date'] != right.iloc[-1]['date']:
left = pd.concat([left, right])
# Only keep the last 1500 candles in memory
left = left[-1500:] if len(left) > 1500 else left
left.reset_index(drop=True, inplace=True)
return left

View File

@@ -15,7 +15,7 @@ from pandas import DataFrame
from freqtrade import constants
from freqtrade.configuration import TimeRange, validate_config_consistency
from freqtrade.constants import DATETIME_PRINT_FORMAT, Config, LongShort
from freqtrade.constants import DATETIME_PRINT_FORMAT, Config, IntOrInf, LongShort
from freqtrade.data import history
from freqtrade.data.btanalysis import find_existing_backtest_stats, trade_list_to_dataframe
from freqtrade.data.converter import trim_dataframe, trim_dataframes
@@ -37,6 +37,7 @@ from freqtrade.plugins.protectionmanager import ProtectionManager
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
from freqtrade.strategy.interface import IStrategy
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
from freqtrade.util.binance_mig import migrate_binance_futures_data
from freqtrade.wallets import Wallets
@@ -157,6 +158,7 @@ class Backtesting:
self._can_short = self.trading_mode != TradingMode.SPOT
self._position_stacking: bool = self.config.get('position_stacking', False)
self.enable_protections: bool = self.config.get('enable_protections', False)
migrate_binance_futures_data(config)
self.init_backtest()
@@ -573,26 +575,6 @@ class Backtesting:
""" Rate is within candle, therefore filled"""
return row[LOW_IDX] <= rate <= row[HIGH_IDX]
def _get_exit_trade_entry_for_candle(self, trade: LocalTrade,
row: Tuple) -> Optional[LocalTrade]:
# Check if we need to adjust our current positions
if self.strategy.position_adjustment_enable:
trade = self._get_adjust_trade_entry_for_candle(trade, row)
enter = row[SHORT_IDX] if trade.is_short else row[LONG_IDX]
exit_sig = row[ESHORT_IDX] if trade.is_short else row[ELONG_IDX]
exits = self.strategy.should_exit(
trade, row[OPEN_IDX], row[DATE_IDX].to_pydatetime(), # type: ignore
enter=enter, exit_=exit_sig,
low=row[LOW_IDX], high=row[HIGH_IDX]
)
for exit_ in exits:
t = self._get_exit_for_signal(trade, row, exit_)
if t:
return t
return None
def _get_exit_for_signal(
self, trade: LocalTrade, row: Tuple, exit_: ExitCheckTuple,
amount: Optional[float] = None) -> Optional[LocalTrade]:
@@ -662,7 +644,7 @@ class Backtesting:
return None
def _exit_trade(self, trade: LocalTrade, sell_row: Tuple,
close_rate: float, amount: float = None) -> Optional[LocalTrade]:
close_rate: float, amount: Optional[float] = None) -> Optional[LocalTrade]:
self.order_id_counter += 1
exit_candle_time = sell_row[DATE_IDX].to_pydatetime()
order_type = self.strategy.order_types['exit']
@@ -692,11 +674,10 @@ class Backtesting:
trade.orders.append(order)
return trade
def _get_exit_trade_entry(
self, trade: LocalTrade, row: Tuple, is_first: bool) -> Optional[LocalTrade]:
def _check_trade_exit(self, trade: LocalTrade, row: Tuple) -> Optional[LocalTrade]:
exit_candle_time: datetime = row[DATE_IDX].to_pydatetime()
if is_first and self.trading_mode == TradingMode.FUTURES:
if self.trading_mode == TradingMode.FUTURES:
trade.funding_fees = self.exchange.calculate_funding_fees(
self.futures_data[trade.pair],
amount=trade.amount,
@@ -705,7 +686,22 @@ class Backtesting:
close_date=exit_candle_time,
)
return self._get_exit_trade_entry_for_candle(trade, row)
# Check if we need to adjust our current positions
if self.strategy.position_adjustment_enable:
trade = self._get_adjust_trade_entry_for_candle(trade, row)
enter = row[SHORT_IDX] if trade.is_short else row[LONG_IDX]
exit_sig = row[ESHORT_IDX] if trade.is_short else row[ELONG_IDX]
exits = self.strategy.should_exit(
trade, row[OPEN_IDX], row[DATE_IDX].to_pydatetime(), # type: ignore
enter=enter, exit_=exit_sig,
low=row[LOW_IDX], high=row[HIGH_IDX]
)
for exit_ in exits:
t = self._get_exit_for_signal(trade, row, exit_)
if t:
return t
return None
def get_valid_price_and_stake(
self, pair: str, row: Tuple, propose_rate: float, stake_amount: float,
@@ -769,6 +765,7 @@ class Backtesting:
stake_amount=stake_amount,
min_stake_amount=min_stake_amount,
max_stake_amount=max_stake_amount,
trade_amount=trade.stake_amount if trade else None
)
return propose_rate, stake_amount_val, leverage, min_stake_amount
@@ -778,6 +775,11 @@ class Backtesting:
trade: Optional[LocalTrade] = None,
requested_rate: Optional[float] = None,
requested_stake: Optional[float] = None) -> Optional[LocalTrade]:
"""
:param trade: Trade to adjust - initial entry if None
:param requested_rate: Adjusted entry rate
:param requested_stake: Stake amount for adjusted orders (`adjust_entry_price`).
"""
current_time = row[DATE_IDX].to_pydatetime()
entry_tag = row[ENTER_TAG_IDX] if len(row) >= ENTER_TAG_IDX + 1 else None
@@ -803,7 +805,7 @@ class Backtesting:
return trade
time_in_force = self.strategy.order_time_in_force['entry']
if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount):
if stake_amount and (not min_stake_amount or stake_amount >= min_stake_amount):
self.order_id_counter += 1
base_currency = self.exchange.get_pair_base_currency(pair)
amount_p = (stake_amount / propose_rate) * leverage
@@ -866,6 +868,7 @@ class Backtesting:
open_rate=propose_rate,
amount=amount,
stake_amount=trade.stake_amount,
leverage=trade.leverage,
wallet_balance=trade.stake_amount,
is_short=is_short,
))
@@ -919,8 +922,9 @@ class Backtesting:
trade.close(exit_row[OPEN_IDX], show_msg=False)
LocalTrade.close_bt_trade(trade)
def trade_slot_available(self, max_open_trades: int, open_trade_count: int) -> bool:
def trade_slot_available(self, open_trade_count: int) -> bool:
# Always allow trades when max_open_trades is enabled.
max_open_trades: IntOrInf = self.config['max_open_trades']
if max_open_trades <= 0 or open_trade_count < max_open_trades:
return True
# Rejected trade
@@ -1050,7 +1054,8 @@ class Backtesting:
def backtest_loop(
self, row: Tuple, pair: str, current_time: datetime, end_date: datetime,
max_open_trades: int, open_trade_count_start: int, is_first: bool = True) -> int:
open_trade_count_start: int, trade_dir: Optional[LongShort],
is_first: bool = True) -> int:
"""
NOTE: This method is used by Hyperopt at each iteration. Please keep it optimized.
@@ -1069,11 +1074,10 @@ class Backtesting:
# max_open_trades must be respected
# don't open on the last row
# We only open trades on the main candle, not on detail candles
trade_dir = self.check_for_trade_entry(row)
if (
(self._position_stacking or len(LocalTrade.bt_trades_open_pp[pair]) == 0)
and is_first
and self.trade_slot_available(max_open_trades, open_trade_count_start)
and self.trade_slot_available(open_trade_count_start)
and current_time != end_date
and trade_dir is not None
and not PairLocks.is_pair_locked(pair, row[DATE_IDX], trade_dir)
@@ -1098,7 +1102,7 @@ class Backtesting:
# 4. Create exit orders (if any)
if not trade.open_order_id:
self._get_exit_trade_entry(trade, row, is_first) # Place exit order if necessary
self._check_trade_exit(trade, row) # Place exit order if necessary
# 5. Process exit orders.
order = trade.select_order(trade.exit_side, is_open=True)
@@ -1120,8 +1124,7 @@ class Backtesting:
return open_trade_count_start
def backtest(self, processed: Dict,
start_date: datetime, end_date: datetime,
max_open_trades: int = 0) -> Dict[str, Any]:
start_date: datetime, end_date: datetime) -> Dict[str, Any]:
"""
Implement backtesting functionality
@@ -1133,7 +1136,6 @@ class Backtesting:
optimize memory usage!
:param start_date: backtesting timerange start datetime
:param end_date: backtesting timerange end datetime
:param max_open_trades: maximum number of concurrent trades, <= 0 means unlimited
:return: DataFrame with trades (results of backtesting)
"""
self.prepare_backtest(self.enable_protections)
@@ -1163,7 +1165,15 @@ class Backtesting:
indexes[pair] = row_index
self.dataprovider._set_dataframe_max_index(row_index)
current_detail_time: datetime = row[DATE_IDX].to_pydatetime()
if self.timeframe_detail and pair in self.detail_data:
trade_dir: Optional[LongShort] = self.check_for_trade_entry(row)
if (
(trade_dir is not None or len(LocalTrade.bt_trades_open_pp[pair]) > 0)
and self.timeframe_detail and pair in self.detail_data
):
# Spread out into detail timeframe.
# Should only happen when we are either in a trade for this pair
# or when we got the signal for a new trade.
exit_candle_end = current_detail_time + timedelta(minutes=self.timeframe_min)
detail_data = self.detail_data[pair]
@@ -1174,8 +1184,9 @@ class Backtesting:
if len(detail_data) == 0:
# Fall back to "regular" data if no detail data was found for this candle
open_trade_count_start = self.backtest_loop(
row, pair, current_time, end_date, max_open_trades,
open_trade_count_start)
row, pair, current_time, end_date,
open_trade_count_start, trade_dir)
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]
@@ -1186,13 +1197,14 @@ class Backtesting:
current_time_det = current_time
for det_row in detail_data[HEADERS].values.tolist():
open_trade_count_start = self.backtest_loop(
det_row, pair, current_time_det, end_date, max_open_trades,
open_trade_count_start, is_first)
det_row, pair, current_time_det, end_date,
open_trade_count_start, trade_dir, is_first)
current_time_det += timedelta(minutes=self.timeframe_detail_min)
is_first = False
else:
open_trade_count_start = self.backtest_loop(
row, pair, current_time, end_date, max_open_trades, open_trade_count_start)
row, pair, current_time, end_date,
open_trade_count_start, trade_dir)
# Move time one configured time_interval ahead.
self.progress.increment()
@@ -1224,13 +1236,11 @@ class Backtesting:
self._set_strategy(strat)
# Use max_open_trades in backtesting, except --disable-max-market-positions is set
if self.config.get('use_max_market_positions', True):
# Must come from strategy config, as the strategy may modify this setting.
max_open_trades = self.strategy.config['max_open_trades']
else:
if not self.config.get('use_max_market_positions', True):
logger.info(
'Ignoring max_open_trades (--disable-max-market-positions was used) ...')
max_open_trades = 0
self.strategy.max_open_trades = float('inf')
self.config.update({'max_open_trades': self.strategy.max_open_trades})
# need to reprocess data every time to populate signals
preprocessed = self.strategy.advise_all_indicators(data)
@@ -1253,7 +1263,6 @@ class Backtesting:
processed=preprocessed,
start_date=min_date,
end_date=max_date,
max_open_trades=max_open_trades,
)
backtest_end_time = datetime.now(timezone.utc)
results.update({

View File

@@ -74,6 +74,7 @@ class Hyperopt:
self.roi_space: List[Dimension] = []
self.stoploss_space: List[Dimension] = []
self.trailing_space: List[Dimension] = []
self.max_open_trades_space: List[Dimension] = []
self.dimensions: List[Dimension] = []
self.config = config
@@ -117,11 +118,10 @@ class Hyperopt:
self.current_best_epoch: Optional[Dict[str, Any]] = None
# Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set
if self.config.get('use_max_market_positions', True):
self.max_open_trades = self.config['max_open_trades']
else:
if not self.config.get('use_max_market_positions', True):
logger.debug('Ignoring max_open_trades (--disable-max-market-positions was used) ...')
self.max_open_trades = 0
self.backtesting.strategy.max_open_trades = float('inf')
config.update({'max_open_trades': self.backtesting.strategy.max_open_trades})
if HyperoptTools.has_space(self.config, 'sell'):
# Make sure use_exit_signal is enabled
@@ -209,6 +209,10 @@ class Hyperopt:
result['stoploss'] = {p.name: params.get(p.name) for p in self.stoploss_space}
if HyperoptTools.has_space(self.config, 'trailing'):
result['trailing'] = self.custom_hyperopt.generate_trailing_params(params)
if HyperoptTools.has_space(self.config, 'trades'):
result['max_open_trades'] = {
'max_open_trades': self.backtesting.strategy.max_open_trades
if self.backtesting.strategy.max_open_trades != float('inf') else -1}
return result
@@ -229,6 +233,8 @@ class Hyperopt:
'trailing_stop_positive_offset': strategy.trailing_stop_positive_offset,
'trailing_only_offset_is_reached': strategy.trailing_only_offset_is_reached,
}
if not HyperoptTools.has_space(self.config, 'trades'):
result['max_open_trades'] = {'max_open_trades': strategy.max_open_trades}
return result
def print_results(self, results) -> None:
@@ -280,8 +286,13 @@ class Hyperopt:
logger.debug("Hyperopt has 'trailing' space")
self.trailing_space = self.custom_hyperopt.trailing_space()
if HyperoptTools.has_space(self.config, 'trades'):
logger.debug("Hyperopt has 'trades' space")
self.max_open_trades_space = self.custom_hyperopt.max_open_trades_space()
self.dimensions = (self.buy_space + self.sell_space + self.protection_space
+ self.roi_space + self.stoploss_space + self.trailing_space)
+ self.roi_space + self.stoploss_space + self.trailing_space
+ self.max_open_trades_space)
def assign_params(self, params_dict: Dict, category: str) -> None:
"""
@@ -328,6 +339,20 @@ class Hyperopt:
self.backtesting.strategy.trailing_only_offset_is_reached = \
d['trailing_only_offset_is_reached']
if HyperoptTools.has_space(self.config, 'trades'):
if self.config["stake_amount"] == "unlimited" and \
(params_dict['max_open_trades'] == -1 or params_dict['max_open_trades'] == 0):
# Ignore unlimited max open trades if stake amount is unlimited
params_dict.update({'max_open_trades': self.config['max_open_trades']})
updated_max_open_trades = int(params_dict['max_open_trades']) \
if (params_dict['max_open_trades'] != -1
and params_dict['max_open_trades'] != 0) else float('inf')
self.config.update({'max_open_trades': updated_max_open_trades})
self.backtesting.strategy.max_open_trades = updated_max_open_trades
with self.data_pickle_file.open('rb') as f:
processed = load(f, mmap_mode='r')
if self.analyze_per_epoch:
@@ -337,8 +362,7 @@ class Hyperopt:
bt_results = self.backtesting.backtest(
processed=processed,
start_date=self.min_date,
end_date=self.max_date,
max_open_trades=self.max_open_trades,
end_date=self.max_date
)
backtest_end_time = datetime.now(timezone.utc)
bt_results.update({

View File

@@ -91,5 +91,8 @@ class HyperOptAuto(IHyperOpt):
def trailing_space(self) -> List['Dimension']:
return self._get_func('trailing_space')()
def max_open_trades_space(self) -> List['Dimension']:
return self._get_func('max_open_trades_space')()
def generate_estimator(self, dimensions: List['Dimension'], **kwargs) -> EstimatorType:
return self._get_func('generate_estimator')(dimensions=dimensions, **kwargs)

View File

@@ -191,6 +191,16 @@ class IHyperOpt(ABC):
Categorical([True, False], name='trailing_only_offset_is_reached'),
]
def max_open_trades_space(self) -> List[Dimension]:
"""
Create a max open trades space.
You may override it in your custom Hyperopt class.
"""
return [
Integer(-1, 10, name='max_open_trades'),
]
# This is needed for proper unpickling the class attribute timeframe
# which is set to the actual value by the resolver.
# Why do I still need such shamanic mantras in modern python?

View File

@@ -5,13 +5,11 @@ This module defines the alternative HyperOptLoss class which can be used for
Hyperoptimization.
"""
from datetime import datetime
from math import sqrt as msqrt
from typing import Any, Dict
from pandas import DataFrame
from freqtrade.constants import Config
from freqtrade.data.metrics import calculate_max_drawdown
from freqtrade.data.metrics import calculate_calmar
from freqtrade.optimize.hyperopt import IHyperOptLoss
@@ -23,42 +21,15 @@ class CalmarHyperOptLoss(IHyperOptLoss):
"""
@staticmethod
def hyperopt_loss_function(
results: DataFrame,
trade_count: int,
min_date: datetime,
max_date: datetime,
config: Config,
processed: Dict[str, DataFrame],
backtest_stats: Dict[str, Any],
*args,
**kwargs
) -> float:
def hyperopt_loss_function(results: DataFrame, trade_count: int,
min_date: datetime, max_date: datetime,
config: Config, *args, **kwargs) -> float:
"""
Objective function, returns smaller number for more optimal results.
Uses Calmar Ratio calculation.
"""
total_profit = backtest_stats["profit_total"]
days_period = (max_date - min_date).days
# adding slippage of 0.1% per trade
total_profit = total_profit - 0.0005
expected_returns_mean = total_profit.sum() / days_period * 100
# calculate max drawdown
try:
_, _, _, _, _, max_drawdown = calculate_max_drawdown(
results, value_col="profit_abs"
)
except ValueError:
max_drawdown = 0
if max_drawdown != 0:
calmar_ratio = expected_returns_mean / max_drawdown * msqrt(365)
else:
# Define high (negative) calmar ratio to be clear that this is NOT optimal.
calmar_ratio = -20.0
starting_balance = config['dry_run_wallet']
calmar_ratio = calculate_calmar(results, min_date, max_date, starting_balance)
# print(expected_returns_mean, max_drawdown, calmar_ratio)
return -calmar_ratio

View File

@@ -6,9 +6,10 @@ Hyperoptimization.
"""
from datetime import datetime
import numpy as np
from pandas import DataFrame
from freqtrade.constants import Config
from freqtrade.data.metrics import calculate_sharpe
from freqtrade.optimize.hyperopt import IHyperOptLoss
@@ -22,25 +23,13 @@ class SharpeHyperOptLoss(IHyperOptLoss):
@staticmethod
def hyperopt_loss_function(results: DataFrame, trade_count: int,
min_date: datetime, max_date: datetime,
*args, **kwargs) -> float:
config: Config, *args, **kwargs) -> float:
"""
Objective function, returns smaller number for more optimal results.
Uses Sharpe Ratio calculation.
"""
total_profit = results["profit_ratio"]
days_period = (max_date - min_date).days
# adding slippage of 0.1% per trade
total_profit = total_profit - 0.0005
expected_returns_mean = total_profit.sum() / days_period
up_stdev = np.std(total_profit)
if up_stdev != 0:
sharp_ratio = expected_returns_mean / up_stdev * np.sqrt(365)
else:
# Define high (negative) sharpe ratio to be clear that this is NOT optimal.
sharp_ratio = -20.
starting_balance = config['dry_run_wallet']
sharp_ratio = calculate_sharpe(results, min_date, max_date, starting_balance)
# print(expected_returns_mean, up_stdev, sharp_ratio)
return -sharp_ratio

View File

@@ -44,7 +44,7 @@ class SharpeHyperOptLossDaily(IHyperOptLoss):
sum_daily = (
results.resample(resample_freq, on='close_date').agg(
{"profit_ratio_after_slippage": sum}).reindex(t_index).fillna(0)
{"profit_ratio_after_slippage": 'sum'}).reindex(t_index).fillna(0)
)
total_profit = sum_daily["profit_ratio_after_slippage"] - risk_free_rate

View File

@@ -6,9 +6,10 @@ Hyperoptimization.
"""
from datetime import datetime
import numpy as np
from pandas import DataFrame
from freqtrade.constants import Config
from freqtrade.data.metrics import calculate_sortino
from freqtrade.optimize.hyperopt import IHyperOptLoss
@@ -22,28 +23,13 @@ class SortinoHyperOptLoss(IHyperOptLoss):
@staticmethod
def hyperopt_loss_function(results: DataFrame, trade_count: int,
min_date: datetime, max_date: datetime,
*args, **kwargs) -> float:
config: Config, *args, **kwargs) -> float:
"""
Objective function, returns smaller number for more optimal results.
Uses Sortino Ratio calculation.
"""
total_profit = results["profit_ratio"]
days_period = (max_date - min_date).days
# adding slippage of 0.1% per trade
total_profit = total_profit - 0.0005
expected_returns_mean = total_profit.sum() / days_period
results['downside_returns'] = 0
results.loc[total_profit < 0, 'downside_returns'] = results['profit_ratio']
down_stdev = np.std(results['downside_returns'])
if down_stdev != 0:
sortino_ratio = expected_returns_mean / down_stdev * np.sqrt(365)
else:
# Define high (negative) sortino ratio to be clear that this is NOT optimal.
sortino_ratio = -20.
starting_balance = config['dry_run_wallet']
sortino_ratio = calculate_sortino(results, min_date, max_date, starting_balance)
# print(expected_returns_mean, down_stdev, sortino_ratio)
return -sortino_ratio

View File

@@ -46,7 +46,7 @@ class SortinoHyperOptLossDaily(IHyperOptLoss):
sum_daily = (
results.resample(resample_freq, on='close_date').agg(
{"profit_ratio_after_slippage": sum}).reindex(t_index).fillna(0)
{"profit_ratio_after_slippage": 'sum'}).reindex(t_index).fillna(0)
)
total_profit = sum_daily["profit_ratio_after_slippage"] - minimum_acceptable_return

View File

@@ -96,7 +96,7 @@ class HyperoptTools():
Tell if the space value is contained in the configuration
"""
# 'trailing' and 'protection spaces are not included in the 'default' set of spaces
if space in ('trailing', 'protection'):
if space in ('trailing', 'protection', 'trades'):
return any(s in config['spaces'] for s in [space, 'all'])
else:
return any(s in config['spaces'] for s in [space, 'all', 'default'])
@@ -170,7 +170,7 @@ class HyperoptTools():
@staticmethod
def show_epoch_details(results, total_epochs: int, print_json: bool,
no_header: bool = False, header_str: str = None) -> None:
no_header: bool = False, header_str: Optional[str] = None) -> None:
"""
Display details of the hyperopt result
"""
@@ -187,7 +187,8 @@ class HyperoptTools():
if print_json:
result_dict: Dict = {}
for s in ['buy', 'sell', 'protection', 'roi', 'stoploss', 'trailing']:
for s in ['buy', 'sell', 'protection',
'roi', 'stoploss', 'trailing', 'max_open_trades']:
HyperoptTools._params_update_for_json(result_dict, params, non_optimized, s)
print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE))
@@ -201,6 +202,8 @@ class HyperoptTools():
HyperoptTools._params_pretty_print(params, 'roi', "ROI table:", non_optimized)
HyperoptTools._params_pretty_print(params, 'stoploss', "Stoploss:", non_optimized)
HyperoptTools._params_pretty_print(params, 'trailing', "Trailing stop:", non_optimized)
HyperoptTools._params_pretty_print(
params, 'max_open_trades', "Max Open Trades:", non_optimized)
@staticmethod
def _params_update_for_json(result_dict, params, non_optimized, space: str) -> None:
@@ -239,7 +242,9 @@ class HyperoptTools():
if space == "stoploss":
stoploss = safe_value_fallback2(space_params, no_params, space, space)
result += (f"stoploss = {stoploss}{appendix}")
elif space == "max_open_trades":
max_open_trades = safe_value_fallback2(space_params, no_params, space, space)
result += (f"max_open_trades = {max_open_trades}{appendix}")
elif space == "roi":
result = result[:-1] + f'{appendix}\n'
minimal_roi_result = rapidjson.dumps({
@@ -259,7 +264,7 @@ class HyperoptTools():
print(result)
@staticmethod
def _space_params(params, space: str, r: int = None) -> Dict:
def _space_params(params, space: str, r: Optional[int] = None) -> Dict:
d = params.get(space)
if d:
# Round floats to `r` digits after the decimal point if requested

View File

@@ -8,9 +8,10 @@ from pandas import DataFrame, to_datetime
from tabulate import tabulate
from freqtrade.constants import (DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN, UNLIMITED_STAKE_AMOUNT,
Config)
from freqtrade.data.metrics import (calculate_cagr, calculate_csum, calculate_market_change,
calculate_max_drawdown)
Config, IntOrInf)
from freqtrade.data.metrics import (calculate_cagr, calculate_calmar, calculate_csum,
calculate_expectancy, calculate_market_change,
calculate_max_drawdown, calculate_sharpe, calculate_sortino)
from freqtrade.misc import decimals_per_coin, file_dump_joblib, file_dump_json, round_coin_value
from freqtrade.optimize.backtest_caching import get_backtest_metadata_filename
@@ -190,7 +191,7 @@ def generate_tag_metrics(tag_type: str,
return []
def generate_exit_reason_stats(max_open_trades: int, results: DataFrame) -> List[Dict]:
def generate_exit_reason_stats(max_open_trades: IntOrInf, results: DataFrame) -> List[Dict]:
"""
Generate small table outlining Backtest results
:param max_open_trades: Max_open_trades parameter
@@ -448,6 +449,10 @@ def generate_strategy_stats(pairlist: List[str],
'profit_total_long_abs': results.loc[~results['is_short'], 'profit_abs'].sum(),
'profit_total_short_abs': results.loc[results['is_short'], 'profit_abs'].sum(),
'cagr': calculate_cagr(backtest_days, start_balance, content['final_balance']),
'expectancy': calculate_expectancy(results),
'sortino': calculate_sortino(results, min_date, max_date, start_balance),
'sharpe': calculate_sharpe(results, min_date, max_date, start_balance),
'calmar': calculate_calmar(results, min_date, max_date, start_balance),
'profit_factor': profit_factor,
'backtest_start': min_date.strftime(DATETIME_PRINT_FORMAT),
'backtest_start_ts': int(min_date.timestamp() * 1000),
@@ -785,8 +790,13 @@ def text_table_add_metrics(strat_results: Dict) -> str:
strat_results['stake_currency'])),
('Total profit %', f"{strat_results['profit_total']:.2%}"),
('CAGR %', f"{strat_results['cagr']:.2%}" if 'cagr' in strat_results else 'N/A'),
('Sortino', f"{strat_results['sortino']:.2f}" if 'sortino' in strat_results else 'N/A'),
('Sharpe', f"{strat_results['sharpe']:.2f}" if 'sharpe' in strat_results else 'N/A'),
('Calmar', f"{strat_results['calmar']:.2f}" if 'calmar' in strat_results else 'N/A'),
('Profit factor', f'{strat_results["profit_factor"]:.2f}' if 'profit_factor'
in strat_results else 'N/A'),
('Expectancy', f"{strat_results['expectancy']:.2f}" if 'expectancy'
in strat_results else 'N/A'),
('Trades per day', strat_results['trades_per_day']),
('Avg. daily profit %',
f"{(strat_results['profit_total'] / strat_results['backtest_days']):.2%}"),

View File

@@ -109,11 +109,10 @@ def migrate_trades_and_orders_table(
else:
is_short = get_column_def(cols, 'is_short', '0')
# Margin Properties
# Futures Properties
interest_rate = get_column_def(cols, 'interest_rate', '0.0')
# Futures properties
funding_fees = get_column_def(cols, 'funding_fees', '0.0')
max_stake_amount = get_column_def(cols, 'max_stake_amount', 'stake_amount')
# If ticker-interval existed use that, else null.
if has_column(cols, 'ticker_interval'):
@@ -162,7 +161,8 @@ def migrate_trades_and_orders_table(
timeframe, open_trade_value, close_profit_abs,
trading_mode, leverage, liquidation_price, is_short,
interest_rate, funding_fees, realized_profit,
amount_precision, price_precision, precision_mode, contract_size
amount_precision, price_precision, precision_mode, contract_size,
max_stake_amount
)
select id, lower(exchange), pair, {base_currency} base_currency,
{stake_currency} stake_currency,
@@ -190,7 +190,8 @@ def migrate_trades_and_orders_table(
{is_short} is_short, {interest_rate} interest_rate,
{funding_fees} funding_fees, {realized_profit} realized_profit,
{amount_precision} amount_precision, {price_precision} price_precision,
{precision_mode} precision_mode, {contract_size} contract_size
{precision_mode} precision_mode, {contract_size} contract_size,
{max_stake_amount} max_stake_amount
from {trade_back_name}
"""))
@@ -213,17 +214,22 @@ def migrate_orders_table(engine, table_back_name: str, cols_order: List):
average = get_column_def(cols_order, 'average', 'null')
stop_price = get_column_def(cols_order, 'stop_price', 'null')
funding_fee = get_column_def(cols_order, 'funding_fee', '0.0')
ft_amount = get_column_def(cols_order, 'ft_amount', 'coalesce(amount, 0.0)')
ft_price = get_column_def(cols_order, 'ft_price', 'coalesce(price, 0.0)')
# sqlite does not support literals for booleans
with engine.begin() as connection:
connection.execute(text(f"""
insert into orders (id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id,
status, symbol, order_type, side, price, amount, filled, average, remaining, cost,
stop_price, order_date, order_filled_date, order_update_date, ft_fee_base, funding_fee)
stop_price, order_date, order_filled_date, order_update_date, ft_fee_base, funding_fee,
ft_amount, ft_price
)
select id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id,
status, symbol, order_type, side, price, amount, filled, {average} average, remaining,
cost, {stop_price} stop_price, order_date, order_filled_date,
order_update_date, {ft_fee_base} ft_fee_base, {funding_fee} funding_fee
order_update_date, {ft_fee_base} ft_fee_base, {funding_fee} funding_fee,
{ft_amount} ft_amount, {ft_price} ft_price
from {table_back_name}
"""))
@@ -310,8 +316,8 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
# if ('orders' not in previous_tables
# or not has_column(cols_orders, 'funding_fee')):
migrating = False
# if not has_column(cols_trades, 'contract_size'):
if not has_column(cols_orders, 'funding_fee'):
# if not has_column(cols_trades, 'max_stake_amount'):
if not has_column(cols_orders, 'ft_price'):
migrating = True
logger.info(f"Running database migration for trades - "
f"backup: {table_back_name}, {order_table_bak_name}")

View File

@@ -21,9 +21,9 @@ class PairLock(_DECL_BASE):
side = Column(String(25), nullable=False, default="*")
reason = Column(String(255), nullable=True)
# Time the pair was locked (start time)
lock_time = Column(DateTime, nullable=False)
lock_time = Column(DateTime(), nullable=False)
# Time until the pair is locked (end time)
lock_end_time = Column(DateTime, nullable=False, index=True)
lock_end_time = Column(DateTime(), nullable=False, index=True)
active = Column(Boolean, nullable=False, default=True, index=True)

View File

@@ -30,8 +30,8 @@ class PairLocks():
PairLocks.locks = []
@staticmethod
def lock_pair(pair: str, until: datetime, reason: str = None, *,
now: datetime = None, side: str = '*') -> PairLock:
def lock_pair(pair: str, until: datetime, reason: Optional[str] = None, *,
now: Optional[datetime] = None, side: str = '*') -> PairLock:
"""
Create PairLock from now to "until".
Uses database by default, unless PairLocks.use_db is set to False,

View File

@@ -46,29 +46,31 @@ class Order(_DECL_BASE):
trade = relationship("Trade", back_populates="orders")
# order_side can only be 'buy', 'sell' or 'stoploss'
ft_order_side: str = Column(String(25), nullable=False)
ft_pair: str = Column(String(25), nullable=False)
ft_order_side = Column(String(25), nullable=False)
ft_pair = Column(String(25), nullable=False)
ft_is_open = Column(Boolean, nullable=False, default=True, index=True)
ft_amount = Column(Float(), nullable=False)
ft_price = Column(Float(), nullable=False)
order_id: str = Column(String(255), nullable=False, index=True)
order_id = Column(String(255), nullable=False, index=True)
status = Column(String(255), nullable=True)
symbol = Column(String(25), nullable=True)
order_type: str = Column(String(50), nullable=True)
order_type = Column(String(50), nullable=True)
side = Column(String(25), nullable=True)
price = Column(Float, nullable=True)
average = Column(Float, nullable=True)
amount = Column(Float, nullable=True)
filled = Column(Float, nullable=True)
remaining = Column(Float, nullable=True)
cost = Column(Float, nullable=True)
stop_price = Column(Float, nullable=True)
order_date = Column(DateTime, nullable=True, default=datetime.utcnow)
order_filled_date = Column(DateTime, nullable=True)
order_update_date = Column(DateTime, nullable=True)
price = Column(Float(), nullable=True)
average = Column(Float(), nullable=True)
amount = Column(Float(), nullable=True)
filled = Column(Float(), nullable=True)
remaining = Column(Float(), nullable=True)
cost = Column(Float(), nullable=True)
stop_price = Column(Float(), nullable=True)
order_date = Column(DateTime(), nullable=True, default=datetime.utcnow)
order_filled_date = Column(DateTime(), nullable=True)
order_update_date = Column(DateTime(), nullable=True)
funding_fee = Column(Float, nullable=True)
funding_fee = Column(Float(), nullable=True)
ft_fee_base = Column(Float, nullable=True)
ft_fee_base = Column(Float(), nullable=True)
@property
def order_date_utc(self) -> datetime:
@@ -82,9 +84,13 @@ class Order(_DECL_BASE):
self.order_filled_date.replace(tzinfo=timezone.utc) if self.order_filled_date else None
)
@property
def safe_amount(self) -> float:
return self.amount or self.ft_amount
@property
def safe_price(self) -> float:
return self.average or self.price or self.stop_price
return self.average or self.price or self.stop_price or self.ft_price
@property
def safe_filled(self) -> float:
@@ -94,7 +100,7 @@ class Order(_DECL_BASE):
def safe_remaining(self) -> float:
return (
self.remaining if self.remaining is not None else
self.amount - (self.filled or 0.0)
self.safe_amount - (self.filled or 0.0)
)
@property
@@ -140,7 +146,7 @@ class Order(_DECL_BASE):
# Assign funding fee up to this point
# (represents the funding fee since the last order)
self.funding_fee = self.trade.funding_fees
if (order.get('filled', 0.0) or 0.0) > 0:
if (order.get('filled', 0.0) or 0.0) > 0 and not self.order_filled_date:
self.order_filled_date = datetime.now(timezone.utc)
self.order_update_date = datetime.now(timezone.utc)
@@ -166,7 +172,7 @@ class Order(_DECL_BASE):
def to_json(self, entry_side: str, minified: bool = False) -> Dict[str, Any]:
resp = {
'amount': self.amount,
'amount': self.safe_amount,
'safe_price': self.safe_price,
'ft_order_side': self.ft_order_side,
'order_filled_timestamp': int(self.order_filled_date.replace(
@@ -227,11 +233,20 @@ class Order(_DECL_BASE):
logger.warning(f"Did not find order for {order}.")
@staticmethod
def parse_from_ccxt_object(order: Dict[str, Any], pair: str, side: str) -> 'Order':
def parse_from_ccxt_object(
order: Dict[str, Any], pair: str, side: str,
amount: Optional[float] = None, price: Optional[float] = None) -> 'Order':
"""
Parse an order from a ccxt object and return a new order Object.
Optional support for overriding amount and price is only used for test simplification.
"""
o = Order(order_id=str(order['id']), ft_order_side=side, ft_pair=pair)
o = Order(
order_id=str(order['id']),
ft_order_side=side,
ft_pair=pair,
ft_amount=amount if amount else order['amount'],
ft_price=price if price else order['price'],
)
o.update_from_ccxt_object(order)
return o
@@ -293,6 +308,7 @@ class LocalTrade():
close_profit: Optional[float] = None
close_profit_abs: Optional[float] = None
stake_amount: float = 0.0
max_stake_amount: float = 0.0
amount: float = 0.0
amount_requested: Optional[float] = None
open_date: datetime
@@ -397,12 +413,6 @@ class LocalTrade():
def close_date_utc(self):
return self.close_date.replace(tzinfo=timezone.utc)
@property
def enter_side(self) -> str:
""" DEPRECATED, please use entry_side instead"""
# TODO: Please remove me after 2022.5
return self.entry_side
@property
def entry_side(self) -> str:
if self.is_short:
@@ -475,8 +485,8 @@ class LocalTrade():
'amount': round(self.amount, 8),
'amount_requested': round(self.amount_requested, 8) if self.amount_requested else None,
'stake_amount': round(self.stake_amount, 8),
'max_stake_amount': round(self.max_stake_amount, 8) if self.max_stake_amount else None,
'strategy': self.strategy,
'buy_tag': self.enter_tag,
'enter_tag': self.enter_tag,
'timeframe': self.timeframe,
@@ -513,7 +523,6 @@ class LocalTrade():
'profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None,
'profit_abs': self.close_profit_abs,
'sell_reason': self.exit_reason, # Deprecated
'exit_reason': self.exit_reason,
'exit_order_status': self.exit_order_status,
'stop_loss_abs': self.stop_loss,
@@ -790,7 +799,7 @@ class LocalTrade():
else:
return close_trade - fees
def calc_close_trade_value(self, rate: float, amount: float = None) -> float:
def calc_close_trade_value(self, rate: float, amount: Optional[float] = None) -> float:
"""
Calculate the Trade's close value including fees
:param rate: rate to compare with.
@@ -828,7 +837,8 @@ class LocalTrade():
raise OperationalException(
f"{self.trading_mode.value} trading is not yet available using freqtrade")
def calc_profit(self, rate: float, amount: float = None, open_rate: float = None) -> float:
def calc_profit(self, rate: float, amount: Optional[float] = None,
open_rate: Optional[float] = None) -> float:
"""
Calculate the absolute profit in stake currency between Close and Open trade
:param rate: close rate to compare with.
@@ -849,7 +859,8 @@ class LocalTrade():
return float(f"{profit:.8f}")
def calc_profit_ratio(
self, rate: float, amount: float = None, open_rate: float = None) -> float:
self, rate: float, amount: Optional[float] = None,
open_rate: Optional[float] = None) -> float:
"""
Calculates the profit as ratio (including fee).
:param rate: rate to compare with.
@@ -882,6 +893,7 @@ class LocalTrade():
ZERO = FtPrecise(0.0)
current_amount = FtPrecise(0.0)
current_stake = FtPrecise(0.0)
max_stake_amount = FtPrecise(0.0)
total_stake = 0.0 # Total stake after all buy orders (does not subtract!)
avg_price = FtPrecise(0.0)
close_profit = 0.0
@@ -923,7 +935,9 @@ class LocalTrade():
exit_rate, amount=exit_amount, open_rate=avg_price)
else:
total_stake = total_stake + self._calc_open_trade_value(tmp_amount, price)
max_stake_amount += (tmp_amount * price)
self.funding_fees = funding_fees
self.max_stake_amount = float(max_stake_amount)
if close_profit:
self.close_profit = close_profit
@@ -959,11 +973,12 @@ class LocalTrade():
return None
def select_order(self, order_side: Optional[str] = None,
is_open: Optional[bool] = None) -> Optional[Order]:
is_open: Optional[bool] = None, only_filled: bool = False) -> Optional[Order]:
"""
Finds latest order for this orderside and status
:param order_side: ft_order_side of the order (either 'buy', 'sell' or 'stoploss')
:param is_open: Only search for open orders?
:param only_filled: Only search for Filled orders (only valid with is_open=False).
:return: latest Order object if it exists, else None
"""
orders = self.orders
@@ -971,6 +986,8 @@ class LocalTrade():
orders = [o for o in orders if o.ft_order_side == order_side]
if is_open is not None:
orders = [o for o in orders if o.ft_is_open == is_open]
if is_open is False and only_filled:
orders = [o for o in orders if o.filled and o.status in NON_OPEN_EXCHANGE_STATES]
if len(orders) > 0:
return orders[-1]
else:
@@ -1044,8 +1061,9 @@ class LocalTrade():
return self.exit_reason
@staticmethod
def get_trades_proxy(*, pair: str = None, is_open: bool = None,
open_date: datetime = None, close_date: datetime = None,
def get_trades_proxy(*, pair: Optional[str] = None, is_open: Optional[bool] = None,
open_date: Optional[datetime] = None,
close_date: Optional[datetime] = None,
) -> List['LocalTrade']:
"""
Helper function to query Trades.
@@ -1159,43 +1177,44 @@ class Trade(_DECL_BASE, LocalTrade):
base_currency = Column(String(25), nullable=True)
stake_currency = Column(String(25), nullable=True)
is_open = Column(Boolean, nullable=False, default=True, index=True)
fee_open = Column(Float, nullable=False, default=0.0)
fee_open_cost = Column(Float, nullable=True)
fee_open = Column(Float(), nullable=False, default=0.0)
fee_open_cost = Column(Float(), nullable=True)
fee_open_currency = Column(String(25), nullable=True)
fee_close = Column(Float, nullable=False, default=0.0)
fee_close_cost = Column(Float, nullable=True)
fee_close = Column(Float(), nullable=False, default=0.0)
fee_close_cost = Column(Float(), nullable=True)
fee_close_currency = Column(String(25), nullable=True)
open_rate: float = Column(Float)
open_rate_requested = Column(Float)
open_rate: float = Column(Float())
open_rate_requested = Column(Float())
# open_trade_value - calculated via _calc_open_trade_value
open_trade_value = Column(Float)
close_rate: Optional[float] = Column(Float)
close_rate_requested = Column(Float)
realized_profit = Column(Float, default=0.0)
close_profit = Column(Float)
close_profit_abs = Column(Float)
stake_amount = Column(Float, nullable=False)
amount = Column(Float)
amount_requested = Column(Float)
open_date = Column(DateTime, nullable=False, default=datetime.utcnow)
close_date = Column(DateTime)
open_trade_value = Column(Float())
close_rate: Optional[float] = Column(Float())
close_rate_requested = Column(Float())
realized_profit = Column(Float(), default=0.0)
close_profit = Column(Float())
close_profit_abs = Column(Float())
stake_amount = Column(Float(), nullable=False)
max_stake_amount = Column(Float())
amount = Column(Float())
amount_requested = Column(Float())
open_date = Column(DateTime(), nullable=False, default=datetime.utcnow)
close_date = Column(DateTime())
open_order_id = Column(String(255))
# absolute value of the stop loss
stop_loss = Column(Float, nullable=True, default=0.0)
stop_loss = Column(Float(), nullable=True, default=0.0)
# percentage value of the stop loss
stop_loss_pct = Column(Float, nullable=True)
stop_loss_pct = Column(Float(), nullable=True)
# absolute value of the initial stop loss
initial_stop_loss = Column(Float, nullable=True, default=0.0)
initial_stop_loss = Column(Float(), nullable=True, default=0.0)
# percentage value of the initial stop loss
initial_stop_loss_pct = Column(Float, nullable=True)
initial_stop_loss_pct = Column(Float(), nullable=True)
# stoploss order id which is on exchange
stoploss_order_id = Column(String(255), nullable=True, index=True)
# last update time of the stoploss order on exchange
stoploss_last_update = Column(DateTime, nullable=True)
stoploss_last_update = Column(DateTime(), nullable=True)
# absolute value of the highest reached price
max_rate = Column(Float, nullable=True, default=0.0)
max_rate = Column(Float(), nullable=True, default=0.0)
# Lowest price reached
min_rate = Column(Float, nullable=True)
min_rate = Column(Float(), nullable=True)
exit_reason = Column(String(100), nullable=True)
exit_order_status = Column(String(100), nullable=True)
strategy = Column(String(100), nullable=True)
@@ -1203,21 +1222,21 @@ class Trade(_DECL_BASE, LocalTrade):
timeframe = Column(Integer, nullable=True)
trading_mode = Column(Enum(TradingMode), nullable=True)
amount_precision = Column(Float, nullable=True)
price_precision = Column(Float, nullable=True)
amount_precision = Column(Float(), nullable=True)
price_precision = Column(Float(), nullable=True)
precision_mode = Column(Integer, nullable=True)
contract_size = Column(Float, nullable=True)
contract_size = Column(Float(), nullable=True)
# Leverage trading properties
leverage = Column(Float, nullable=True, default=1.0)
leverage = Column(Float(), nullable=True, default=1.0)
is_short = Column(Boolean, nullable=False, default=False)
liquidation_price = Column(Float, nullable=True)
liquidation_price = Column(Float(), nullable=True)
# Margin Trading Properties
interest_rate = Column(Float, nullable=False, default=0.0)
interest_rate = Column(Float(), nullable=False, default=0.0)
# Futures properties
funding_fees = Column(Float, nullable=True, default=None)
funding_fees = Column(Float(), nullable=True, default=None)
def __init__(self, **kwargs):
super().__init__(**kwargs)
@@ -1241,8 +1260,9 @@ class Trade(_DECL_BASE, LocalTrade):
Trade.query.session.rollback()
@staticmethod
def get_trades_proxy(*, pair: str = None, is_open: bool = None,
open_date: datetime = None, close_date: datetime = None,
def get_trades_proxy(*, pair: Optional[str] = None, is_open: Optional[bool] = None,
open_date: Optional[datetime] = None,
close_date: Optional[datetime] = None,
) -> List['LocalTrade']:
"""
Helper function to query Trades.j

View File

@@ -436,11 +436,11 @@ def create_scatter(
return None
def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFrame = None, *,
indicators1: List[str] = [],
indicators2: List[str] = [],
plot_config: Dict[str, Dict] = {},
) -> go.Figure:
def generate_candlestick_graph(
pair: str, data: pd.DataFrame, trades: Optional[pd.DataFrame] = None, *,
indicators1: List[str] = [], indicators2: List[str] = [],
plot_config: Dict[str, Dict] = {},
) -> go.Figure:
"""
Generate the graph from the data generated by Backtesting or from DB
Volume will always be ploted in row2, so Row 1 and 3 are to our disposal for custom indicators

View File

@@ -0,0 +1,206 @@
"""
Remote PairList provider
Provides pair list fetched from a remote source
"""
import json
import logging
from pathlib import Path
from typing import Any, Dict, List, Tuple
import requests
from cachetools import TTLCache
from freqtrade import __version__
from freqtrade.constants import Config
from freqtrade.exceptions import OperationalException
from freqtrade.exchange.types import Tickers
from freqtrade.plugins.pairlist.IPairList import IPairList
logger = logging.getLogger(__name__)
class RemotePairList(IPairList):
def __init__(self, exchange, pairlistmanager,
config: Config, pairlistconfig: Dict[str, Any],
pairlist_pos: int) -> None:
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
if 'number_assets' not in self._pairlistconfig:
raise OperationalException(
'`number_assets` not specified. Please check your configuration '
'for "pairlist.config.number_assets"')
if 'pairlist_url' not in self._pairlistconfig:
raise OperationalException(
'`pairlist_url` not specified. Please check your configuration '
'for "pairlist.config.pairlist_url"')
self._number_pairs = self._pairlistconfig['number_assets']
self._refresh_period: int = self._pairlistconfig.get('refresh_period', 1800)
self._keep_pairlist_on_failure = self._pairlistconfig.get('keep_pairlist_on_failure', True)
self._pair_cache: TTLCache = TTLCache(maxsize=1, ttl=self._refresh_period)
self._pairlist_url = self._pairlistconfig.get('pairlist_url', '')
self._read_timeout = self._pairlistconfig.get('read_timeout', 60)
self._bearer_token = self._pairlistconfig.get('bearer_token', '')
self._init_done = False
self._last_pairlist: List[Any] = list()
@property
def needstickers(self) -> bool:
"""
Boolean property defining if tickers are necessary.
If no Pairlist requires tickers, an empty Dict is passed
as tickers argument to filter_pairlist
"""
return False
def short_desc(self) -> str:
"""
Short whitelist method description - used for startup-messages
"""
return f"{self.name} - {self._pairlistconfig['number_assets']} pairs from RemotePairlist."
def process_json(self, jsonparse) -> List[str]:
pairlist = jsonparse.get('pairs', [])
remote_refresh_period = int(jsonparse.get('refresh_period', self._refresh_period))
if self._refresh_period < remote_refresh_period:
self.log_once(f'Refresh Period has been increased from {self._refresh_period}'
f' to minimum allowed: {remote_refresh_period} from Remote.', logger.info)
self._refresh_period = remote_refresh_period
self._pair_cache = TTLCache(maxsize=1, ttl=remote_refresh_period)
self._init_done = True
return pairlist
def return_last_pairlist(self) -> List[str]:
if self._keep_pairlist_on_failure:
pairlist = self._last_pairlist
self.log_once('Keeping last fetched pairlist', logger.info)
else:
pairlist = []
return pairlist
def fetch_pairlist(self) -> Tuple[List[str], float]:
headers = {
'User-Agent': 'Freqtrade/' + __version__ + ' Remotepairlist'
}
if self._bearer_token:
headers['Authorization'] = f'Bearer {self._bearer_token}'
try:
response = requests.get(self._pairlist_url, headers=headers,
timeout=self._read_timeout)
content_type = response.headers.get('content-type')
time_elapsed = response.elapsed.total_seconds()
if "application/json" in str(content_type):
jsonparse = response.json()
try:
pairlist = self.process_json(jsonparse)
except Exception as e:
if self._init_done:
pairlist = self.return_last_pairlist()
logger.warning(f'Error while processing JSON data: {type(e)}')
else:
raise OperationalException(f'Error while processing JSON data: {type(e)}')
else:
if self._init_done:
self.log_once(f'Error: RemotePairList is not of type JSON: '
f' {self._pairlist_url}', logger.info)
pairlist = self.return_last_pairlist()
else:
raise OperationalException('RemotePairList is not of type JSON, abort.')
except requests.exceptions.RequestException:
self.log_once(f'Was not able to fetch pairlist from:'
f' {self._pairlist_url}', logger.info)
pairlist = self.return_last_pairlist()
time_elapsed = 0
return pairlist, time_elapsed
def gen_pairlist(self, tickers: Tickers) -> List[str]:
"""
Generate the pairlist
:param tickers: Tickers (from exchange.get_tickers). May be cached.
:return: List of pairs
"""
if self._init_done:
pairlist = self._pair_cache.get('pairlist')
else:
pairlist = []
time_elapsed = 0.0
if pairlist:
# Item found - no refresh necessary
return pairlist.copy()
else:
if self._pairlist_url.startswith("file:///"):
filename = self._pairlist_url.split("file:///", 1)[1]
file_path = Path(filename)
if file_path.exists():
with open(filename) as json_file:
# Load the JSON data into a dictionary
jsonparse = json.load(json_file)
try:
pairlist = self.process_json(jsonparse)
except Exception as e:
if self._init_done:
pairlist = self.return_last_pairlist()
logger.warning(f'Error while processing JSON data: {type(e)}')
else:
raise OperationalException('Error while processing'
f'JSON data: {type(e)}')
else:
raise ValueError(f"{self._pairlist_url} does not exist.")
else:
# Fetch Pairlist from Remote URL
pairlist, time_elapsed = self.fetch_pairlist()
self.log_once(f"Fetched pairs: {pairlist}", logger.debug)
pairlist = self._whitelist_for_active_markets(pairlist)
pairlist = pairlist[:self._number_pairs]
self._pair_cache['pairlist'] = pairlist.copy()
if time_elapsed != 0.0:
self.log_once(f'Pairlist Fetched in {time_elapsed} seconds.', logger.info)
else:
self.log_once('Fetched Pairlist.', logger.info)
self._last_pairlist = list(pairlist)
return pairlist
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
"""
Filters and sorts pairlist and returns the whitelist again.
Called on each bot iteration - please use internal caching if necessary
:param pairlist: pairlist to filter or sort
:param tickers: Tickers (from exchange.get_tickers). May be cached.
:return: new whitelist
"""
rpl_pairlist = self.gen_pairlist(tickers)
merged_list = pairlist + rpl_pairlist
merged_list = sorted(set(merged_list), key=merged_list.index)
return merged_list

View File

@@ -135,7 +135,7 @@ class VolumePairList(IPairList):
filtered_tickers = [
v for k, v in tickers.items()
if (self._exchange.get_pair_quote_currency(k) == self._stake_currency
and (self._use_range or v[self._sort_key] is not None)
and (self._use_range or v.get(self._sort_key) is not None)
and v['symbol'] in _pairlist)]
pairlist = [s['symbol'] for s in filtered_tickers]
else:

View File

@@ -23,7 +23,8 @@ logger = logging.getLogger(__name__)
class PairListManager(LoggingMixin):
def __init__(self, exchange, config: Config, dataprovider: DataProvider = None) -> None:
def __init__(
self, exchange, config: Config, dataprovider: Optional[DataProvider] = None) -> None:
self._exchange = exchange
self._config = config
self._whitelist = self._config['exchange'].get('pair_whitelist')
@@ -153,7 +154,8 @@ class PairListManager(LoggingMixin):
return []
return whitelist
def create_pair_list(self, pairs: List[str], timeframe: str = None) -> ListPairsWithTimeframes:
def create_pair_list(
self, pairs: List[str], timeframe: Optional[str] = None) -> ListPairsWithTimeframes:
"""
Create list of pair tuples with (pair, timeframe)
"""

View File

@@ -89,7 +89,8 @@ class IResolver:
module = importlib.util.module_from_spec(spec)
try:
spec.loader.exec_module(module) # type: ignore # importlib does not use typehints
except (ModuleNotFoundError, SyntaxError, ImportError, NameError) as err:
except (AttributeError, ModuleNotFoundError, SyntaxError,
ImportError, NameError) as err:
# Catch errors in case a specific module is not installed
logger.warning(f"Could not import {module_path} due to '{err}'")
if enum_failed:

View File

@@ -33,7 +33,7 @@ class StrategyResolver(IResolver):
extra_path = "strategy_path"
@staticmethod
def load_strategy(config: Config = None) -> IStrategy:
def load_strategy(config: Optional[Config] = None) -> IStrategy:
"""
Load the custom class from config parameter
:param config: configuration dictionary or None
@@ -76,6 +76,7 @@ class StrategyResolver(IResolver):
("ignore_buying_expired_candle_after", 0),
("position_adjustment_enable", False),
("max_entry_position_adjustment", -1),
("max_open_trades", -1)
]
for attribute, default in attributes:
StrategyResolver._override_attribute_helper(strategy, config,
@@ -110,7 +111,11 @@ class StrategyResolver(IResolver):
val = getattr(strategy, attribute)
# None's cannot exist in the config, so do not copy them
if val is not None:
config[attribute] = val
# max_open_trades set to -1 in the strategy will be copied as infinity in the config
if attribute == 'max_open_trades' and val == -1:
config[attribute] = float('inf')
else:
config[attribute] = val
# Explicitly check for None here as other "falsy" values are possible
elif default is not None:
setattr(strategy, attribute, default)
@@ -128,6 +133,8 @@ class StrategyResolver(IResolver):
key=lambda t: t[0]))
if hasattr(strategy, 'stoploss'):
strategy.stoploss = float(strategy.stoploss)
if hasattr(strategy, 'max_open_trades') and strategy.max_open_trades < 0:
strategy.max_open_trades = float('inf')
return strategy
@staticmethod

View File

@@ -11,6 +11,7 @@ from freqtrade.configuration.config_validation import validate_config_consistenc
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.misc import deep_merge_dicts
from freqtrade.rpc.api_server.api_schemas import (BacktestHistoryEntry, BacktestRequest,
BacktestResponse)
from freqtrade.rpc.api_server.deps import get_config, is_webserver_mode
@@ -37,10 +38,11 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac
btconfig = deepcopy(config)
settings = dict(bt_settings)
if settings.get('freqai', None) is not None:
settings['freqai'] = dict(settings['freqai'])
# Pydantic models will contain all keys, but non-provided ones are None
for setting in settings.keys():
if settings[setting] is not None:
btconfig[setting] = settings[setting]
btconfig = deep_merge_dicts(settings, btconfig, allow_null_overrides=False)
try:
btconfig['stake_amount'] = float(btconfig['stake_amount'])
except ValueError:

View File

@@ -3,7 +3,7 @@ from typing import Any, Dict, List, Optional, Union
from pydantic import BaseModel
from freqtrade.constants import DATETIME_PRINT_FORMAT
from freqtrade.constants import DATETIME_PRINT_FORMAT, IntOrInf
from freqtrade.enums import OrderTypeValues, SignalDirection, TradingMode
@@ -165,9 +165,10 @@ class ShowConfig(BaseModel):
stake_amount: str
available_capital: Optional[float]
stake_currency_decimals: int
max_open_trades: int
max_open_trades: IntOrInf
minimal_roi: Dict[str, Any]
stoploss: Optional[float]
stoploss_on_exchange: bool
trailing_stop: Optional[bool]
trailing_stop_positive: Optional[float]
trailing_stop_positive_offset: Optional[float]
@@ -217,8 +218,8 @@ class TradeSchema(BaseModel):
amount: float
amount_requested: float
stake_amount: float
max_stake_amount: Optional[float]
strategy: str
buy_tag: Optional[str] # Deprecated
enter_tag: Optional[str]
timeframe: int
fee_open: Optional[float]
@@ -243,7 +244,6 @@ class TradeSchema(BaseModel):
profit_pct: Optional[float]
profit_abs: Optional[float]
profit_fiat: Optional[float]
sell_reason: Optional[str] # Deprecated
exit_reason: Optional[str]
exit_order_status: Optional[str]
stop_loss_abs: Optional[float]
@@ -372,6 +372,10 @@ class StrategyListResponse(BaseModel):
strategies: List[str]
class FreqAIModelListResponse(BaseModel):
freqaimodels: List[str]
class StrategyResponse(BaseModel):
strategy: str
code: str
@@ -410,15 +414,22 @@ class PairHistory(BaseModel):
}
class BacktestFreqAIInputs(BaseModel):
identifier: str
class BacktestRequest(BaseModel):
strategy: str
timeframe: Optional[str]
timeframe_detail: Optional[str]
timerange: Optional[str]
max_open_trades: Optional[int]
max_open_trades: Optional[IntOrInf]
stake_amount: Optional[str]
enable_protections: bool
dry_run_wallet: Optional[float]
backtest_cache: Optional[str]
freqaimodel: Optional[str]
freqai: Optional[BacktestFreqAIInputs]
class BacktestResponse(BaseModel):

View File

@@ -13,12 +13,13 @@ from freqtrade.rpc import RPC
from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, BlacklistPayload,
BlacklistResponse, Count, Daily,
DeleteLockRequest, DeleteTrade, ForceEnterPayload,
ForceEnterResponse, ForceExitPayload, Health,
Locks, Logs, OpenTradeSchema, PairHistory,
PerformanceEntry, Ping, PlotConfig, Profit,
ResultMsg, ShowConfig, Stats, StatusMsg,
StrategyListResponse, StrategyResponse, SysInfo,
Version, WhitelistResponse)
ForceEnterResponse, ForceExitPayload,
FreqAIModelListResponse, Health, Locks, Logs,
OpenTradeSchema, PairHistory, PerformanceEntry,
Ping, PlotConfig, Profit, ResultMsg, ShowConfig,
Stats, StatusMsg, StrategyListResponse,
StrategyResponse, SysInfo, Version,
WhitelistResponse)
from freqtrade.rpc.api_server.deps import get_config, get_exchange, get_rpc, get_rpc_optional
from freqtrade.rpc.rpc import RPCException
@@ -38,7 +39,10 @@ logger = logging.getLogger(__name__)
# 2.17: Forceentry - leverage, partial force_exit
# 2.20: Add websocket endpoints
# 2.21: Add new_candle messagetype
API_VERSION = 2.21
# 2.22: Add FreqAI to backtesting
# 2.23: Allow plot config request in webserver mode
# 2.24: Add cancel_open_order endpoint
API_VERSION = 2.24
# Public API, requires no auth.
router_public = APIRouter()
@@ -120,6 +124,12 @@ def trades_delete(tradeid: int, rpc: RPC = Depends(get_rpc)):
return rpc._rpc_delete(tradeid)
@router.delete('/trades/{tradeid}/open-order', response_model=OpenTradeSchema, tags=['trading'])
def cancel_open_order(tradeid: int, rpc: RPC = Depends(get_rpc)):
rpc._rpc_cancel_open_order(tradeid)
return rpc._rpc_trade_status([tradeid])[0]
# TODO: Missing response model
@router.get('/edge', tags=['info'])
def edge(rpc: RPC = Depends(get_rpc)):
@@ -246,8 +256,18 @@ def pair_history(pair: str, timeframe: str, timerange: str, strategy: str,
@router.get('/plot_config', response_model=PlotConfig, tags=['candle data'])
def plot_config(rpc: RPC = Depends(get_rpc)):
return PlotConfig.parse_obj(rpc._rpc_plot_config())
def plot_config(strategy: Optional[str] = None, config=Depends(get_config),
rpc: Optional[RPC] = Depends(get_rpc_optional)):
if not strategy:
if not rpc:
raise RPCException("Strategy is mandatory in webserver mode.")
return PlotConfig.parse_obj(rpc._rpc_plot_config())
else:
config1 = deepcopy(config)
config1.update({
'strategy': strategy
})
return PlotConfig.parse_obj(RPC._rpc_plot_config_with_strategy(config1))
@router.get('/strategies', response_model=StrategyListResponse, tags=['strategy'])
@@ -279,6 +299,16 @@ def get_strategy(strategy: str, config=Depends(get_config)):
}
@router.get('/freqaimodels', response_model=FreqAIModelListResponse, tags=['freqai'])
def list_freqaimodels(config=Depends(get_config)):
from freqtrade.resolvers.freqaimodel_resolver import FreqaiModelResolver
strategies = FreqaiModelResolver.search_all_objects(
config, False)
strategies = sorted(strategies, key=lambda x: x['name'])
return {'freqaimodels': [x['name'] for x in strategies]}
@router.get('/available_pairs', response_model=AvailablePairs, tags=['candle data'])
def list_available_pairs(timeframe: Optional[str] = None, stake_currency: Optional[str] = None,
candletype: Optional[CandleType] = None, config=Depends(get_config)):

View File

@@ -90,10 +90,11 @@ async def _process_consumer_request(
elif type == RPCRequestType.ANALYZED_DF:
# Limit the amount of candles per dataframe to 'limit' or 1500
limit = min(data.get('limit', 1500), 1500) if data else None
limit = int(min(data.get('limit', 1500), 1500)) if data else None
pair = data.get('pair', None) if data else None
# For every pair in the generator, send a separate message
for message in rpc._ws_request_analyzed_df(limit):
for message in rpc._ws_request_analyzed_df(limit, pair):
# Format response
response = WSAnalyzedDFMessage(data=message)
await channel.send(response.dict(exclude_none=True))

View File

@@ -27,7 +27,8 @@ class WebSocketChannel:
self,
websocket: WebSocketType,
channel_id: Optional[str] = None,
serializer_cls: Type[WebSocketSerializer] = HybridJSONWebSocketSerializer
serializer_cls: Type[WebSocketSerializer] = HybridJSONWebSocketSerializer,
send_throttle: float = 0.01
):
self.channel_id = channel_id if channel_id else uuid4().hex[:8]
self._websocket = WebSocketProxy(websocket)
@@ -41,6 +42,7 @@ class WebSocketChannel:
self._send_times: Deque[float] = deque([], maxlen=10)
# High limit defaults to 3 to start
self._send_high_limit = 3
self._send_throttle = send_throttle
# The subscribed message types
self._subscriptions: List[str] = []
@@ -106,7 +108,8 @@ class WebSocketChannel:
# Explicitly give control back to event loop as
# websockets.send does not
await asyncio.sleep(0.01)
# Also throttles how fast we send
await asyncio.sleep(self._send_throttle)
async def recv(self):
"""

View File

@@ -47,7 +47,7 @@ class WSWhitelistRequest(WSRequestSchema):
class WSAnalyzedDFRequest(WSRequestSchema):
type: RPCRequestType = RPCRequestType.ANALYZED_DF
data: Dict[str, Any] = {"limit": 1500}
data: Dict[str, Any] = {"limit": 1500, "pair": None}
# ------------------------------ MESSAGE SCHEMAS ----------------------------

View File

@@ -8,15 +8,17 @@ import asyncio
import logging
import socket
from threading import Thread
from typing import TYPE_CHECKING, Any, Callable, Dict, List, TypedDict
from typing import TYPE_CHECKING, Any, Callable, Dict, List, TypedDict, Union
import websockets
from pydantic import ValidationError
from freqtrade.constants import FULL_DATAFRAME_THRESHOLD
from freqtrade.data.dataprovider import DataProvider
from freqtrade.enums import RPCMessageType
from freqtrade.misc import remove_entry_exit_signals
from freqtrade.rpc.api_server.ws import WebSocketChannel
from freqtrade.rpc.api_server.ws.channel import WebSocketChannel, create_channel
from freqtrade.rpc.api_server.ws.message_stream import MessageStream
from freqtrade.rpc.api_server.ws_schemas import (WSAnalyzedDFMessage, WSAnalyzedDFRequest,
WSMessageSchema, WSRequestSchema,
WSSubscribeRequest, WSWhitelistMessage,
@@ -38,6 +40,10 @@ class Producer(TypedDict):
logger = logging.getLogger(__name__)
def schema_to_dict(schema: Union[WSMessageSchema, WSRequestSchema]):
return schema.dict(exclude_none=True)
class ExternalMessageConsumer:
"""
The main controller class for consuming external messages from
@@ -92,6 +98,8 @@ class ExternalMessageConsumer:
RPCMessageType.ANALYZED_DF: self._consume_analyzed_df_message,
}
self._channel_streams: Dict[str, MessageStream] = {}
self.start()
def start(self):
@@ -118,6 +126,8 @@ class ExternalMessageConsumer:
logger.info("Stopping ExternalMessageConsumer")
self._running = False
self._channel_streams = {}
if self._sub_tasks:
# Cancel sub tasks
for task in self._sub_tasks:
@@ -175,7 +185,6 @@ class ExternalMessageConsumer:
:param producer: Dictionary containing producer info
:param lock: An asyncio Lock
"""
channel = None
while self._running:
try:
host, port = producer['host'], producer['port']
@@ -190,19 +199,21 @@ class ExternalMessageConsumer:
max_size=self.message_size_limit,
ping_interval=None
) as ws:
channel = WebSocketChannel(ws, channel_id=name)
async with create_channel(
ws,
channel_id=name,
send_throttle=0.5
) as channel:
logger.info(f"Producer connection success - {channel}")
# Create the message stream for this channel
self._channel_streams[name] = MessageStream()
# Now request the initial data from this Producer
for request in self._initial_requests:
await channel.send(
request.dict(exclude_none=True)
# Run the channel tasks while connected
await channel.run_channel_tasks(
self._receive_messages(channel, producer, lock),
self._send_requests(channel, self._channel_streams[name])
)
# Now receive data, if none is within the time limit, ping
await self._receive_messages(channel, producer, lock)
except (websockets.exceptions.InvalidURI, ValueError) as e:
logger.error(f"{ws_url} is an invalid WebSocket URL - {e}")
break
@@ -229,11 +240,19 @@ class ExternalMessageConsumer:
# An unforseen error has occurred, log and continue
logger.error("Unexpected error has occurred:")
logger.exception(e)
await asyncio.sleep(self.sleep_time)
continue
finally:
if channel:
await channel.close()
async def _send_requests(self, channel: WebSocketChannel, channel_stream: MessageStream):
# Send the initial requests
for init_request in self._initial_requests:
await channel.send(schema_to_dict(init_request))
# Now send any subsequent requests published to
# this channel's stream
async for request, _ in channel_stream:
logger.debug(f"Sending request to channel - {channel} - {request}")
await channel.send(request)
async def _receive_messages(
self,
@@ -270,19 +289,31 @@ class ExternalMessageConsumer:
latency = (await asyncio.wait_for(pong, timeout=self.ping_timeout) * 1000)
logger.info(f"Connection to {channel} still alive, latency: {latency}ms")
continue
except (websockets.exceptions.ConnectionClosed):
# Just eat the error and continue reconnecting
logger.warning(f"Disconnection in {channel} - retrying in {self.sleep_time}s")
await asyncio.sleep(self.sleep_time)
break
except Exception as e:
# Just eat the error and continue reconnecting
logger.warning(f"Ping error {channel} - {e} - retrying in {self.sleep_time}s")
logger.debug(e, exc_info=e)
await asyncio.sleep(self.sleep_time)
raise
break
def send_producer_request(
self,
producer_name: str,
request: Union[WSRequestSchema, Dict[str, Any]]
):
"""
Publish a message to the producer's message stream to be
sent by the channel task.
:param producer_name: The name of the producer to publish the message to
:param request: The request to send to the producer
"""
if isinstance(request, WSRequestSchema):
request = schema_to_dict(request)
if channel_stream := self._channel_streams.get(producer_name):
channel_stream.publish(request)
def handle_producer_message(self, producer: Producer, message: Dict[str, Any]):
"""
@@ -336,16 +367,45 @@ class ExternalMessageConsumer:
pair, timeframe, candle_type = key
if df.empty:
logger.debug(f"Received Empty Dataframe for {key}")
return
# If set, remove the Entry and Exit signals from the Producer
if self._emc_config.get('remove_entry_exit_signals', False):
df = remove_entry_exit_signals(df)
# Add the dataframe to the dataprovider
self._dp._add_external_df(pair, df,
last_analyzed=la,
timeframe=timeframe,
candle_type=candle_type,
producer_name=producer_name)
logger.debug(f"Received {len(df)} candle(s) for {key}")
did_append, n_missing = self._dp._add_external_df(
pair,
df,
last_analyzed=la,
timeframe=timeframe,
candle_type=candle_type,
producer_name=producer_name
)
if not did_append:
# We want an overlap in candles incase some data has changed
n_missing += 1
# Set to None for all candles if we missed a full df's worth of candles
n_missing = n_missing if n_missing < FULL_DATAFRAME_THRESHOLD else 1500
logger.warning(f"Holes in data or no existing df, requesting {n_missing} candles "
f"for {key} from `{producer_name}`")
self.send_producer_request(
producer_name,
WSAnalyzedDFRequest(
data={
"limit": n_missing,
"pair": pair
}
)
)
return
logger.debug(
f"Consumed message from `{producer_name}` of type `RPCMessageType.ANALYZED_DF`")
f"Consumed message from `{producer_name}` "
f"of type `RPCMessageType.ANALYZED_DF` for {key}")

View File

@@ -122,6 +122,7 @@ class RPC:
if config['max_open_trades'] != float('inf') else -1),
'minimal_roi': config['minimal_roi'].copy() if 'minimal_roi' in config else {},
'stoploss': config.get('stoploss'),
'stoploss_on_exchange': config.get('stoploss_on_exchange', False),
'trailing_stop': config.get('trailing_stop'),
'trailing_stop_positive': config.get('trailing_stop_positive'),
'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset'),
@@ -167,6 +168,7 @@ class RPC:
results = []
for trade in trades:
order: Optional[Order] = None
current_profit_fiat: Optional[float] = None
if trade.open_order_id:
order = trade.select_order_by_order_id(trade.open_order_id)
# calculate profit and send message to user
@@ -176,23 +178,26 @@ class RPC:
trade.pair, side='exit', is_short=trade.is_short, refresh=False)
except (ExchangeError, PricingError):
current_rate = NAN
if len(trade.select_filled_orders(trade.entry_side)) > 0:
current_profit = trade.calc_profit_ratio(
current_rate) if not isnan(current_rate) else NAN
current_profit_abs = trade.calc_profit(
current_rate) if not isnan(current_rate) else NAN
else:
current_profit = current_profit_abs = current_profit_fiat = 0.0
else:
# Closed trade ...
current_rate = trade.close_rate
if len(trade.select_filled_orders(trade.entry_side)) > 0:
current_profit = trade.calc_profit_ratio(
current_rate) if not isnan(current_rate) else NAN
current_profit_abs = trade.calc_profit(
current_rate) if not isnan(current_rate) else NAN
current_profit_fiat: Optional[float] = None
# Calculate fiat profit
if self._fiat_converter:
current_profit_fiat = self._fiat_converter.convert_amount(
current_profit_abs,
self._freqtrade.config['stake_currency'],
self._freqtrade.config['fiat_display_currency']
)
else:
current_profit = current_profit_abs = current_profit_fiat = 0.0
current_profit = trade.close_profit
current_profit_abs = trade.close_profit_abs
# Calculate fiat profit
if not isnan(current_profit_abs) and self._fiat_converter:
current_profit_fiat = self._fiat_converter.convert_amount(
current_profit_abs,
self._freqtrade.config['stake_currency'],
self._freqtrade.config['fiat_display_currency']
)
# Calculate guaranteed profit (in case of trailing stop)
stoploss_entry_dist = trade.calc_profit(trade.stop_loss)
@@ -669,6 +674,7 @@ class RPC:
if self._freqtrade.state == State.RUNNING:
# Set 'max_open_trades' to 0
self._freqtrade.config['max_open_trades'] = 0
self._freqtrade.strategy.max_open_trades = 0
return {'status': 'No more entries will occur from now. Run /reload_config to reset.'}
@@ -807,6 +813,29 @@ class RPC:
else:
raise RPCException(f'Failed to enter position for {pair}.')
def _rpc_cancel_open_order(self, trade_id: int):
if self._freqtrade.state != State.RUNNING:
raise RPCException('trader is not running')
with self._freqtrade._exit_lock:
# Query for trade
trade = Trade.get_trades(
trade_filter=[Trade.id == trade_id, Trade.is_open.is_(True), ]
).first()
if not trade:
logger.warning('cancel_open_order: Invalid trade_id received.')
raise RPCException('Invalid trade_id.')
if not trade.open_order_id:
logger.warning('cancel_open_order: No open order for trade_id.')
raise RPCException('No open order for trade_id.')
try:
order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair)
except ExchangeError as e:
logger.info(f"Cannot query order for {trade} due to {e}.", exc_info=True)
raise RPCException("Order not found.")
self._freqtrade.handle_cancel_order(order, trade, CANCEL_REASON['USER_CANCEL'])
Trade.commit()
def _rpc_delete(self, trade_id: int) -> Dict[str, Union[str, int]]:
"""
Handler for delete <id>.
@@ -940,7 +969,7 @@ class RPC:
resp['errors'] = errors
return resp
def _rpc_blacklist(self, add: List[str] = None) -> Dict:
def _rpc_blacklist(self, add: Optional[List[str]] = None) -> Dict:
""" Returns the currently active blacklist"""
errors = {}
if add:
@@ -1058,15 +1087,26 @@ class RPC:
return self._convert_dataframe_to_dict(self._freqtrade.config['strategy'],
pair, timeframe, _data, last_analyzed)
def __rpc_analysed_dataframe_raw(self, pair: str, timeframe: str,
limit: Optional[int]) -> Tuple[DataFrame, datetime]:
""" Get the dataframe and last analyze from the dataprovider """
def __rpc_analysed_dataframe_raw(
self,
pair: str,
timeframe: str,
limit: Optional[int]
) -> Tuple[DataFrame, datetime]:
"""
Get the dataframe and last analyze from the dataprovider
:param pair: The pair to get
:param timeframe: The timeframe of data to get
:param limit: The amount of candles in the dataframe
"""
_data, last_analyzed = self._freqtrade.dataprovider.get_analyzed_dataframe(
pair, timeframe)
_data = _data.copy()
if limit:
_data = _data.iloc[-limit:]
return _data, last_analyzed
def _ws_all_analysed_dataframes(
@@ -1074,7 +1114,16 @@ class RPC:
pairlist: List[str],
limit: Optional[int]
) -> Generator[Dict[str, Any], None, None]:
""" Get the analysed dataframes of each pair in the pairlist """
"""
Get the analysed dataframes of each pair in the pairlist.
If specified, only return the most recent `limit` candles for
each dataframe.
:param pairlist: A list of pairs to get
:param limit: If an integer, limits the size of dataframe
If a list of string date times, only returns those candles
:returns: A generator of dictionaries with the key, dataframe, and last analyzed timestamp
"""
timeframe = self._freqtrade.config['timeframe']
candle_type = self._freqtrade.config.get('candle_type_def', CandleType.SPOT)
@@ -1087,22 +1136,27 @@ class RPC:
"la": last_analyzed
}
def _ws_request_analyzed_df(self, limit: Optional[int]):
def _ws_request_analyzed_df(
self,
limit: Optional[int] = None,
pair: Optional[str] = None
):
""" Historical Analyzed Dataframes for WebSocket """
whitelist = self._freqtrade.active_pair_whitelist
return self._ws_all_analysed_dataframes(whitelist, limit)
pairlist = [pair] if pair else self._freqtrade.active_pair_whitelist
return self._ws_all_analysed_dataframes(pairlist, limit)
def _ws_request_whitelist(self):
""" Whitelist data for WebSocket """
return self._freqtrade.active_pair_whitelist
@staticmethod
def _rpc_analysed_history_full(config, pair: str, timeframe: str,
def _rpc_analysed_history_full(config: Config, pair: str, timeframe: str,
timerange: str, exchange) -> Dict[str, Any]:
timerange_parsed = TimeRange.parse_timerange(timerange)
_data = load_data(
datadir=config.get("datadir"),
datadir=config["datadir"],
pairs=[pair],
timeframe=timeframe,
timerange=timerange_parsed,
@@ -1127,6 +1181,16 @@ class RPC:
self._freqtrade.strategy.plot_config['subplots'] = {}
return self._freqtrade.strategy.plot_config
@staticmethod
def _rpc_plot_config_with_strategy(config: Config) -> Dict[str, Any]:
from freqtrade.resolvers.strategy_resolver import StrategyResolver
strategy = StrategyResolver.load_strategy(config)
if (strategy.plot_config and 'subplots' not in strategy.plot_config):
strategy.plot_config['subplots'] = {}
return strategy.plot_config
@staticmethod
def _rpc_sysinfo() -> Dict[str, Any]:
return {

View File

@@ -174,6 +174,7 @@ class Telegram(RPCHandler):
self._force_enter, order_side=SignalDirection.SHORT)),
CommandHandler('trades', self._trades),
CommandHandler('delete', self._delete_trade),
CommandHandler(['coo', 'cancel_open_order'], self._cancel_open_order),
CommandHandler('performance', self._performance),
CommandHandler(['buys', 'entries'], self._enter_tag_performance),
CommandHandler(['sells', 'exits'], self._exit_reason_performance),
@@ -1144,10 +1145,25 @@ class Telegram(RPCHandler):
raise RPCException("Trade-id not set.")
trade_id = int(context.args[0])
msg = self._rpc._rpc_delete(trade_id)
self._send_msg((
self._send_msg(
f"`{msg['result_msg']}`\n"
'Please make sure to take care of this asset on the exchange manually.'
))
)
@authorized_only
def _cancel_open_order(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /cancel_open_order <id>.
Cancel open order for tradeid
:param bot: telegram bot
:param update: message update
:return: None
"""
if not context.args or len(context.args) == 0:
raise RPCException("Trade-id not set.")
trade_id = int(context.args[0])
self._rpc._rpc_cancel_open_order(trade_id)
self._send_msg('Open order canceled.')
@authorized_only
def _performance(self, update: Update, context: CallbackContext) -> None:
@@ -1456,6 +1472,10 @@ class Telegram(RPCHandler):
"*/fx <trade_id>|all:* `Alias to /forceexit`\n"
f"{force_enter_text if self._config.get('force_entry_enable', False) else ''}"
"*/delete <trade_id>:* `Instantly delete the given trade in the database`\n"
"*/cancel_open_order <trade_id>:* `Cancels open orders for trade. "
"Only valid when the trade has open orders.`\n"
"*/coo <trade_id>|all:* `Alias to /cancel_open_order`\n"
"*/whitelist [sorted] [baseonly]:* `Show current whitelist. Optionally in "
"order and/or only displaying the base currency of each pairing.`\n"
"*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs "
@@ -1605,7 +1625,7 @@ class Telegram(RPCHandler):
def _send_msg(self, msg: str, parse_mode: str = ParseMode.MARKDOWN,
disable_notification: bool = False,
keyboard: List[List[InlineKeyboardButton]] = None,
keyboard: Optional[List[List[InlineKeyboardButton]]] = None,
callback_path: str = "",
reload_able: bool = False,
query: Optional[CallbackQuery] = None) -> None:

View File

@@ -4,7 +4,7 @@ This module defines a base class for auto-hyperoptable strategies.
"""
import logging
from pathlib import Path
from typing import Any, Dict, Iterator, List, Tuple, Type, Union
from typing import Any, Dict, Iterator, List, Optional, Tuple, Type, Union
from freqtrade.constants import Config
from freqtrade.exceptions import OperationalException
@@ -36,7 +36,8 @@ class HyperStrategyMixin:
self._ft_params_from_file = params
# Init/loading of parameters is done as part of ft_bot_start().
def enumerate_parameters(self, category: str = None) -> Iterator[Tuple[str, BaseParameter]]:
def enumerate_parameters(
self, category: Optional[str] = None) -> Iterator[Tuple[str, BaseParameter]]:
"""
Find all optimizable parameters and return (name, attr) iterator.
:param category:
@@ -80,6 +81,8 @@ class HyperStrategyMixin:
self.stoploss = params.get('stoploss', {}).get(
'stoploss', getattr(self, 'stoploss', -0.1))
self.max_open_trades = params.get('max_open_trades', {}).get(
'max_open_trades', getattr(self, 'max_open_trades', -1))
trailing = params.get('trailing', {})
self.trailing_stop = trailing.get(
'trailing_stop', getattr(self, 'trailing_stop', False))
@@ -160,7 +163,7 @@ class HyperStrategyMixin:
else:
logger.info(f'Strategy Parameter(default): {attr_name} = {attr.value}')
def get_no_optimize_params(self):
def get_no_optimize_params(self) -> Dict[str, Dict]:
"""
Returns list of Parameters that are not part of the current optimize job
"""
@@ -170,7 +173,7 @@ class HyperStrategyMixin:
'protection': {},
}
for name, p in self.enumerate_parameters():
if not p.optimize or not p.in_space:
if p.category and (not p.optimize or not p.in_space):
params[p.category][name] = p.value
return params

View File

@@ -10,7 +10,7 @@ from typing import Dict, List, Optional, Tuple, Union
import arrow
from pandas import DataFrame
from freqtrade.constants import Config, ListPairsWithTimeframes
from freqtrade.constants import Config, IntOrInf, ListPairsWithTimeframes
from freqtrade.data.dataprovider import DataProvider
from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, RunMode, SignalDirection,
SignalTagType, SignalType, TradingMode)
@@ -54,6 +54,9 @@ class IStrategy(ABC, HyperStrategyMixin):
# associated stoploss
stoploss: float
# max open trades for the strategy
max_open_trades: IntOrInf
# trailing stoploss
trailing_stop: bool = False
trailing_stop_positive: Optional[float] = None
@@ -595,9 +598,10 @@ class IStrategy(ABC, HyperStrategyMixin):
return None
def populate_any_indicators(self, pair: str, df: DataFrame, tf: str,
informative: DataFrame = None,
informative: Optional[DataFrame] = None,
set_generalized_indicators: bool = False) -> DataFrame:
"""
DEPRECATED - USE FEATURE ENGINEERING FUNCTIONS INSTEAD
Function designed to automatically generate, name and merge features
from user indicated timeframes in the configuration file. User can add
additional features here, but must follow the naming convention.
@@ -610,6 +614,102 @@ class IStrategy(ABC, HyperStrategyMixin):
"""
return df
def feature_engineering_expand_all(self, dataframe: DataFrame, period: int,
metadata: Dict, **kwargs):
"""
*Only functional with FreqAI enabled strategies*
This function will automatically expand the defined features on the config defined
`indicator_periods_candles`, `include_timeframes`, `include_shifted_candles`, and
`include_corr_pairs`. In other words, a single feature defined in this function
will automatically expand to a total of
`indicator_periods_candles` * `include_timeframes` * `include_shifted_candles` *
`include_corr_pairs` numbers of features added to the model.
All features must be prepended with `%` to be recognized by FreqAI internals.
More details on how these config defined parameters accelerate feature engineering
in the documentation at:
https://www.freqtrade.io/en/latest/freqai-parameter-table/#feature-parameters
https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features
:param dataframe: strategy dataframe which will receive the features
:param period: period of the indicator - usage example:
:param metadata: metadata of current pair
dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period)
"""
return dataframe
def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata: Dict, **kwargs):
"""
*Only functional with FreqAI enabled strategies*
This function will automatically expand the defined features on the config defined
`include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`.
In other words, a single feature defined in this function
will automatically expand to a total of
`include_timeframes` * `include_shifted_candles` * `include_corr_pairs`
numbers of features added to the model.
Features defined here will *not* be automatically duplicated on user defined
`indicator_periods_candles`
All features must be prepended with `%` to be recognized by FreqAI internals.
More details on how these config defined parameters accelerate feature engineering
in the documentation at:
https://www.freqtrade.io/en/latest/freqai-parameter-table/#feature-parameters
https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features
:param dataframe: strategy dataframe which will receive the features
:param metadata: metadata of current pair
dataframe["%-pct-change"] = dataframe["close"].pct_change()
dataframe["%-ema-200"] = ta.EMA(dataframe, timeperiod=200)
"""
return dataframe
def feature_engineering_standard(self, dataframe: DataFrame, metadata: Dict, **kwargs):
"""
*Only functional with FreqAI enabled strategies*
This optional function will be called once with the dataframe of the base timeframe.
This is the final function to be called, which means that the dataframe entering this
function will contain all the features and columns created by all other
freqai_feature_engineering_* functions.
This function is a good place to do custom exotic feature extractions (e.g. tsfresh).
This function is a good place for any feature that should not be auto-expanded upon
(e.g. day of the week).
All features must be prepended with `%` to be recognized by FreqAI internals.
More details about feature engineering available:
https://www.freqtrade.io/en/latest/freqai-feature-engineering
:param dataframe: strategy dataframe which will receive the features
:param metadata: metadata of current pair
usage example: dataframe["%-day_of_week"] = (dataframe["date"].dt.dayofweek + 1) / 7
"""
return dataframe
def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs):
"""
*Only functional with FreqAI enabled strategies*
Required function to set the targets for the model.
All targets must be prepended with `&` to be recognized by the FreqAI internals.
More details about feature engineering available:
https://www.freqtrade.io/en/latest/freqai-feature-engineering
:param dataframe: strategy dataframe which will receive the targets
:param metadata: metadata of current pair
usage example: dataframe["&-target"] = dataframe["close"].shift(-1) / dataframe["close"]
"""
return dataframe
###
# END - Intended to be overridden by strategy
###
@@ -663,7 +763,8 @@ class IStrategy(ABC, HyperStrategyMixin):
"""
return self.__class__.__name__
def lock_pair(self, pair: str, until: datetime, reason: str = None, side: str = '*') -> None:
def lock_pair(self, pair: str, until: datetime,
reason: Optional[str] = None, side: str = '*') -> None:
"""
Locks pair until a given timestamp happens.
Locked pairs are not analyzed, and are prevented from opening new trades.
@@ -695,7 +796,8 @@ class IStrategy(ABC, HyperStrategyMixin):
"""
PairLocks.unlock_reason(reason, datetime.now(timezone.utc))
def is_pair_locked(self, pair: str, *, candle_date: datetime = None, side: str = '*') -> bool:
def is_pair_locked(self, pair: str, *, candle_date: Optional[datetime] = None,
side: str = '*') -> bool:
"""
Checks if a pair is currently locked
The 2nd, optional parameter ensures that locks are applied until the new candle arrives,
@@ -866,7 +968,7 @@ class IStrategy(ABC, HyperStrategyMixin):
pair: str,
timeframe: str,
dataframe: DataFrame,
is_short: bool = None
is_short: Optional[bool] = None
) -> Tuple[bool, bool, Optional[str]]:
"""
Calculates current exit signal based based on the dataframe
@@ -965,7 +1067,7 @@ class IStrategy(ABC, HyperStrategyMixin):
def should_exit(self, trade: Trade, rate: float, current_time: datetime, *,
enter: bool, exit_: bool,
low: float = None, high: float = None,
low: Optional[float] = None, high: Optional[float] = None,
force_stoploss: float = 0) -> List[ExitCheckTuple]:
"""
This function evaluates if one of the conditions required to trigger an exit order
@@ -981,10 +1083,10 @@ class IStrategy(ABC, HyperStrategyMixin):
trade.adjust_min_max_rates(high or current_rate, low or current_rate)
stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade,
current_time=current_time,
current_profit=current_profit,
force_stoploss=force_stoploss, low=low, high=high)
stoplossflag = self.ft_stoploss_reached(current_rate=current_rate, trade=trade,
current_time=current_time,
current_profit=current_profit,
force_stoploss=force_stoploss, low=low, high=high)
# Set current rate to high for backtesting exits
current_rate = (low if trade.is_short else high) or rate
@@ -1051,13 +1153,12 @@ class IStrategy(ABC, HyperStrategyMixin):
return exits
def stop_loss_reached(self, current_rate: float, trade: Trade,
current_time: datetime, current_profit: float,
force_stoploss: float, low: float = None,
high: float = None) -> ExitCheckTuple:
def ft_stoploss_adjust(self, current_rate: float, trade: Trade,
current_time: datetime, current_profit: float,
force_stoploss: float, low: Optional[float] = None,
high: Optional[float] = None) -> None:
"""
Based on current profit of the trade and configured (trailing) stoploss,
decides to exit or not
Adjust stop-loss dynamically if configured to do so.
:param current_profit: current profit as ratio
:param low: Low value of this candle, only set in backtesting
:param high: High value of this candle, only set in backtesting
@@ -1103,6 +1204,20 @@ class IStrategy(ABC, HyperStrategyMixin):
trade.adjust_stop_loss(bound or current_rate, stop_loss_value)
def ft_stoploss_reached(self, current_rate: float, trade: Trade,
current_time: datetime, current_profit: float,
force_stoploss: float, low: Optional[float] = None,
high: Optional[float] = None) -> ExitCheckTuple:
"""
Based on current profit of the trade and configured (trailing) stoploss,
decides to exit or not
:param current_profit: current profit as ratio
:param low: Low value of this candle, only set in backtesting
:param high: High value of this candle, only set in backtesting
"""
self.ft_stoploss_adjust(current_rate, trade, current_time, current_profit,
force_stoploss, low, high)
sl_higher_long = (trade.stop_loss >= (low or current_rate) and not trade.is_short)
sl_lower_short = (trade.stop_loss <= (high or current_rate) and trade.is_short)
liq_higher_long = (trade.liquidation_price

View File

@@ -1,4 +1,5 @@
import logging
from typing import Dict
import numpy as np
import pandas as pd
@@ -95,65 +96,137 @@ class FreqaiExampleHybridStrategy(IStrategy):
short_rsi = IntParameter(low=51, high=100, default=70, space='sell', optimize=True, load=True)
exit_short_rsi = IntParameter(low=1, high=50, default=30, space='buy', optimize=True, load=True)
# FreqAI required function, user can add or remove indicators, but general structure
# must stay the same.
def populate_any_indicators(
self, pair, df, tf, informative=None, set_generalized_indicators=False
):
def feature_engineering_expand_all(self, dataframe: DataFrame, period: int,
metadata: Dict, **kwargs):
"""
User feeds these indicators to FreqAI to train a classifier to decide
if the market will go up or down.
*Only functional with FreqAI enabled strategies*
This function will automatically expand the defined features on the config defined
`indicator_periods_candles`, `include_timeframes`, `include_shifted_candles`, and
`include_corr_pairs`. In other words, a single feature defined in this function
will automatically expand to a total of
`indicator_periods_candles` * `include_timeframes` * `include_shifted_candles` *
`include_corr_pairs` numbers of features added to the model.
:param pair: pair to be used as informative
:param df: strategy dataframe which will receive merges from informatives
:param tf: timeframe of the dataframe which will modify the feature names
:param informative: the dataframe associated with the informative pair
All features must be prepended with `%` to be recognized by FreqAI internals.
More details on how these config defined parameters accelerate feature engineering
in the documentation at:
https://www.freqtrade.io/en/latest/freqai-parameter-table/#feature-parameters
https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features
:param dataframe: strategy dataframe which will receive the features
:param period: period of the indicator - usage example:
:param metadata: metadata of current pair
dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period)
"""
if informative is None:
informative = self.dp.get_pair_dataframe(pair, tf)
dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period)
dataframe["%-mfi-period"] = ta.MFI(dataframe, timeperiod=period)
dataframe["%-adx-period"] = ta.ADX(dataframe, timeperiod=period)
dataframe["%-sma-period"] = ta.SMA(dataframe, timeperiod=period)
dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period)
# first loop is automatically duplicating indicators for time periods
for t in self.freqai_info["feature_parameters"]["indicator_periods_candles"]:
bollinger = qtpylib.bollinger_bands(
qtpylib.typical_price(dataframe), window=period, stds=2.2
)
dataframe["bb_lowerband-period"] = bollinger["lower"]
dataframe["bb_middleband-period"] = bollinger["mid"]
dataframe["bb_upperband-period"] = bollinger["upper"]
t = int(t)
informative[f"%-{pair}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t)
informative[f"%-{pair}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t)
informative[f"%-{pair}adx-period_{t}"] = ta.ADX(informative, timeperiod=t)
informative[f"%-{pair}sma-period_{t}"] = ta.SMA(informative, timeperiod=t)
informative[f"%-{pair}ema-period_{t}"] = ta.EMA(informative, timeperiod=t)
informative[f"%-{pair}roc-period_{t}"] = ta.ROC(informative, timeperiod=t)
informative[f"%-{pair}relative_volume-period_{t}"] = (
informative["volume"] / informative["volume"].rolling(t).mean()
)
dataframe["%-bb_width-period"] = (
dataframe["bb_upperband-period"]
- dataframe["bb_lowerband-period"]
) / dataframe["bb_middleband-period"]
dataframe["%-close-bb_lower-period"] = (
dataframe["close"] / dataframe["bb_lowerband-period"]
)
# FreqAI needs the following lines in order to detect features and automatically
# expand upon them.
indicators = [col for col in informative if col.startswith("%")]
# This loop duplicates and shifts all indicators to add a sense of recency to data
for n in range(self.freqai_info["feature_parameters"]["include_shifted_candles"] + 1):
if n == 0:
continue
informative_shift = informative[indicators].shift(n)
informative_shift = informative_shift.add_suffix("_shift-" + str(n))
informative = pd.concat((informative, informative_shift), axis=1)
dataframe["%-roc-period"] = ta.ROC(dataframe, timeperiod=period)
df = merge_informative_pair(df, informative, self.config["timeframe"], tf, ffill=True)
skip_columns = [
(s + "_" + tf) for s in ["date", "open", "high", "low", "close", "volume"]
]
df = df.drop(columns=skip_columns)
dataframe["%-relative_volume-period"] = (
dataframe["volume"] / dataframe["volume"].rolling(period).mean()
)
# User can set the "target" here (in present case it is the
# "up" or "down")
if set_generalized_indicators:
# User "looks into the future" here to figure out if the future
# will be "up" or "down". This same column name is available to
# the user
df['&s-up_or_down'] = np.where(df["close"].shift(-50) >
df["close"], 'up', 'down')
return dataframe
return df
def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata: Dict, **kwargs):
"""
*Only functional with FreqAI enabled strategies*
This function will automatically expand the defined features on the config defined
`include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`.
In other words, a single feature defined in this function
will automatically expand to a total of
`include_timeframes` * `include_shifted_candles` * `include_corr_pairs`
numbers of features added to the model.
Features defined here will *not* be automatically duplicated on user defined
`indicator_periods_candles`
All features must be prepended with `%` to be recognized by FreqAI internals.
More details on how these config defined parameters accelerate feature engineering
in the documentation at:
https://www.freqtrade.io/en/latest/freqai-parameter-table/#feature-parameters
https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features
:param dataframe: strategy dataframe which will receive the features
:param metadata: metadata of current pair
dataframe["%-pct-change"] = dataframe["close"].pct_change()
dataframe["%-ema-200"] = ta.EMA(dataframe, timeperiod=200)
"""
dataframe["%-pct-change"] = dataframe["close"].pct_change()
dataframe["%-raw_volume"] = dataframe["volume"]
dataframe["%-raw_price"] = dataframe["close"]
return dataframe
def feature_engineering_standard(self, dataframe: DataFrame, metadata: Dict, **kwargs):
"""
*Only functional with FreqAI enabled strategies*
This optional function will be called once with the dataframe of the base timeframe.
This is the final function to be called, which means that the dataframe entering this
function will contain all the features and columns created by all other
freqai_feature_engineering_* functions.
This function is a good place to do custom exotic feature extractions (e.g. tsfresh).
This function is a good place for any feature that should not be auto-expanded upon
(e.g. day of the week).
All features must be prepended with `%` to be recognized by FreqAI internals.
More details about feature engineering available:
https://www.freqtrade.io/en/latest/freqai-feature-engineering
:param dataframe: strategy dataframe which will receive the features
:param metadata: metadata of current pair
usage example: dataframe["%-day_of_week"] = (dataframe["date"].dt.dayofweek + 1) / 7
"""
dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek
dataframe["%-hour_of_day"] = dataframe["date"].dt.hour
return dataframe
def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs):
"""
*Only functional with FreqAI enabled strategies*
Required function to set the targets for the model.
All targets must be prepended with `&` to be recognized by the FreqAI internals.
More details about feature engineering available:
https://www.freqtrade.io/en/latest/freqai-feature-engineering
:param dataframe: strategy dataframe which will receive the targets
:param metadata: metadata of current pair
usage example: dataframe["&-target"] = dataframe["close"].shift(-1) / dataframe["close"]
"""
dataframe['&s-up_or_down'] = np.where(dataframe["close"].shift(-50) >
dataframe["close"], 'up', 'down')
return dataframe
# flake8: noqa: C901
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:

View File

@@ -1,12 +1,12 @@
import logging
from functools import reduce
from typing import Dict
import pandas as pd
import talib.abstract as ta
from pandas import DataFrame
from technical import qtpylib
from freqtrade.strategy import CategoricalParameter, IStrategy, merge_informative_pair
from freqtrade.strategy import CategoricalParameter, IStrategy
logger = logging.getLogger(__name__)
@@ -18,8 +18,8 @@ class FreqaiExampleStrategy(IStrategy):
IFreqaiModel to the strategy. Namely, the user uses:
self.freqai.start(dataframe, metadata)
to make predictions on their data. populate_any_indicators() automatically
generates the variety of features indicated by the user in the
to make predictions on their data. feature_engineering_*() automatically
generate the variety of features indicated by the user in the
canonical freqtrade configuration file under config['freqai'].
"""
@@ -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"},
},
@@ -40,133 +40,200 @@ class FreqaiExampleStrategy(IStrategy):
use_exit_signal = True
# this is the maximum period fed to talib (timeframe independent)
startup_candle_count: int = 40
can_short = False
can_short = True
std_dev_multiplier_buy = CategoricalParameter(
[0.75, 1, 1.25, 1.5, 1.75], default=1.25, space="buy", optimize=True)
std_dev_multiplier_sell = CategoricalParameter(
[0.75, 1, 1.25, 1.5, 1.75], space="sell", default=1.25, optimize=True)
def populate_any_indicators(
self, pair, df, tf, informative=None, set_generalized_indicators=False
):
def feature_engineering_expand_all(self, dataframe: DataFrame, period: int,
metadata: Dict, **kwargs):
"""
Function designed to automatically generate, name and merge features
from user indicated timeframes in the configuration file. User controls the indicators
passed to the training/prediction by prepending indicators with `f'%-{pair}`
(see convention below). I.e. user should not prepend any supporting metrics
(e.g. bb_lowerband below) with % unless they explicitly want to pass that metric to the
model.
:param pair: pair to be used as informative
:param df: strategy dataframe which will receive merges from informatives
:param tf: timeframe of the dataframe which will modify the feature names
:param informative: the dataframe associated with the informative pair
*Only functional with FreqAI enabled strategies*
This function will automatically expand the defined features on the config defined
`indicator_periods_candles`, `include_timeframes`, `include_shifted_candles`, and
`include_corr_pairs`. In other words, a single feature defined in this function
will automatically expand to a total of
`indicator_periods_candles` * `include_timeframes` * `include_shifted_candles` *
`include_corr_pairs` numbers of features added to the model.
All features must be prepended with `%` to be recognized by FreqAI internals.
Access metadata such as the current pair/timeframe with:
`metadata["pair"]` `metadata["tf"]`
More details on how these config defined parameters accelerate feature engineering
in the documentation at:
https://www.freqtrade.io/en/latest/freqai-parameter-table/#feature-parameters
https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features
:param dataframe: strategy dataframe which will receive the features
:param period: period of the indicator - usage example:
:param metadata: metadata of current pair
dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period)
"""
if informative is None:
informative = self.dp.get_pair_dataframe(pair, tf)
dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period)
dataframe["%-mfi-period"] = ta.MFI(dataframe, timeperiod=period)
dataframe["%-adx-period"] = ta.ADX(dataframe, timeperiod=period)
dataframe["%-sma-period"] = ta.SMA(dataframe, timeperiod=period)
dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period)
# first loop is automatically duplicating indicators for time periods
for t in self.freqai_info["feature_parameters"]["indicator_periods_candles"]:
bollinger = qtpylib.bollinger_bands(
qtpylib.typical_price(dataframe), window=period, stds=2.2
)
dataframe["bb_lowerband-period"] = bollinger["lower"]
dataframe["bb_middleband-period"] = bollinger["mid"]
dataframe["bb_upperband-period"] = bollinger["upper"]
t = int(t)
informative[f"%-{pair}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t)
informative[f"%-{pair}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t)
informative[f"%-{pair}adx-period_{t}"] = ta.ADX(informative, timeperiod=t)
informative[f"%-{pair}sma-period_{t}"] = ta.SMA(informative, timeperiod=t)
informative[f"%-{pair}ema-period_{t}"] = ta.EMA(informative, timeperiod=t)
dataframe["%-bb_width-period"] = (
dataframe["bb_upperband-period"]
- dataframe["bb_lowerband-period"]
) / dataframe["bb_middleband-period"]
dataframe["%-close-bb_lower-period"] = (
dataframe["close"] / dataframe["bb_lowerband-period"]
)
bollinger = qtpylib.bollinger_bands(
qtpylib.typical_price(informative), window=t, stds=2.2
)
informative[f"{pair}bb_lowerband-period_{t}"] = bollinger["lower"]
informative[f"{pair}bb_middleband-period_{t}"] = bollinger["mid"]
informative[f"{pair}bb_upperband-period_{t}"] = bollinger["upper"]
dataframe["%-roc-period"] = ta.ROC(dataframe, timeperiod=period)
informative[f"%-{pair}bb_width-period_{t}"] = (
informative[f"{pair}bb_upperband-period_{t}"]
- informative[f"{pair}bb_lowerband-period_{t}"]
) / informative[f"{pair}bb_middleband-period_{t}"]
informative[f"%-{pair}close-bb_lower-period_{t}"] = (
informative["close"] / informative[f"{pair}bb_lowerband-period_{t}"]
dataframe["%-relative_volume-period"] = (
dataframe["volume"] / dataframe["volume"].rolling(period).mean()
)
return dataframe
def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata: Dict, **kwargs):
"""
*Only functional with FreqAI enabled strategies*
This function will automatically expand the defined features on the config defined
`include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`.
In other words, a single feature defined in this function
will automatically expand to a total of
`include_timeframes` * `include_shifted_candles` * `include_corr_pairs`
numbers of features added to the model.
Features defined here will *not* be automatically duplicated on user defined
`indicator_periods_candles`
All features must be prepended with `%` to be recognized by FreqAI internals.
Access metadata such as the current pair/timeframe with:
`metadata["pair"]` `metadata["tf"]`
More details on how these config defined parameters accelerate feature engineering
in the documentation at:
https://www.freqtrade.io/en/latest/freqai-parameter-table/#feature-parameters
https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features
:param dataframe: strategy dataframe which will receive the features
:param metadata: metadata of current pair
dataframe["%-pct-change"] = dataframe["close"].pct_change()
dataframe["%-ema-200"] = ta.EMA(dataframe, timeperiod=200)
"""
dataframe["%-pct-change"] = dataframe["close"].pct_change()
dataframe["%-raw_volume"] = dataframe["volume"]
dataframe["%-raw_price"] = dataframe["close"]
return dataframe
def feature_engineering_standard(self, dataframe: DataFrame, metadata: Dict, **kwargs):
"""
*Only functional with FreqAI enabled strategies*
This optional function will be called once with the dataframe of the base timeframe.
This is the final function to be called, which means that the dataframe entering this
function will contain all the features and columns created by all other
freqai_feature_engineering_* functions.
This function is a good place to do custom exotic feature extractions (e.g. tsfresh).
This function is a good place for any feature that should not be auto-expanded upon
(e.g. day of the week).
All features must be prepended with `%` to be recognized by FreqAI internals.
Access metadata such as the current pair with:
`metadata["pair"]`
More details about feature engineering available:
https://www.freqtrade.io/en/latest/freqai-feature-engineering
:param dataframe: strategy dataframe which will receive the features
:param metadata: metadata of current pair
usage example: dataframe["%-day_of_week"] = (dataframe["date"].dt.dayofweek + 1) / 7
"""
dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek
dataframe["%-hour_of_day"] = dataframe["date"].dt.hour
return dataframe
def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs):
"""
*Only functional with FreqAI enabled strategies*
Required function to set the targets for the model.
All targets must be prepended with `&` to be recognized by the FreqAI internals.
Access metadata such as the current pair with:
`metadata["pair"]`
More details about feature engineering available:
https://www.freqtrade.io/en/latest/freqai-feature-engineering
:param dataframe: strategy dataframe which will receive the targets
:param metadata: metadata of current pair
usage example: dataframe["&-target"] = dataframe["close"].shift(-1) / dataframe["close"]
"""
dataframe["&-s_close"] = (
dataframe["close"]
.shift(-self.freqai_info["feature_parameters"]["label_period_candles"])
.rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
.mean()
/ dataframe["close"]
- 1
)
informative[f"%-{pair}roc-period_{t}"] = ta.ROC(informative, timeperiod=t)
# Classifiers are typically set up with strings as targets:
# df['&s-up_or_down'] = np.where( df["close"].shift(-100) >
# df["close"], 'up', 'down')
informative[f"%-{pair}relative_volume-period_{t}"] = (
informative["volume"] / informative["volume"].rolling(t).mean()
)
# 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
# freqai/prediction_models/CatboostRegressorMultiTarget.py,
# freqtrade trade --freqaimodel CatboostRegressorMultiTarget
informative[f"%-{pair}pct-change"] = informative["close"].pct_change()
informative[f"%-{pair}raw_volume"] = informative["volume"]
informative[f"%-{pair}raw_price"] = informative["close"]
# df["&-s_range"] = (
# df["close"]
# .shift(-self.freqai_info["feature_parameters"]["label_period_candles"])
# .rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
# .max()
# -
# df["close"]
# .shift(-self.freqai_info["feature_parameters"]["label_period_candles"])
# .rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
# .min()
# )
indicators = [col for col in informative if col.startswith("%")]
# This loop duplicates and shifts all indicators to add a sense of recency to data
for n in range(self.freqai_info["feature_parameters"]["include_shifted_candles"] + 1):
if n == 0:
continue
informative_shift = informative[indicators].shift(n)
informative_shift = informative_shift.add_suffix("_shift-" + str(n))
informative = pd.concat((informative, informative_shift), axis=1)
df = merge_informative_pair(df, informative, self.config["timeframe"], tf, ffill=True)
skip_columns = [
(s + "_" + tf) for s in ["date", "open", "high", "low", "close", "volume"]
]
df = df.drop(columns=skip_columns)
# Add generalized indicators here (because in live, it will call this
# function to populate indicators during training). Notice how we ensure not to
# add them multiple times
if set_generalized_indicators:
df["%-day_of_week"] = (df["date"].dt.dayofweek + 1) / 7
df["%-hour_of_day"] = (df["date"].dt.hour + 1) / 25
# user adds targets here by prepending them with &- (see convention below)
df["&-s_close"] = (
df["close"]
.shift(-self.freqai_info["feature_parameters"]["label_period_candles"])
.rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
.mean()
/ df["close"]
- 1
)
# Classifiers are typically set up with strings as targets:
# df['&s-up_or_down'] = np.where( df["close"].shift(-100) >
# df["close"], 'up', 'down')
# 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,
# df["&-s_range"] = (
# df["close"]
# .shift(-self.freqai_info["feature_parameters"]["label_period_candles"])
# .rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
# .max()
# -
# df["close"]
# .shift(-self.freqai_info["feature_parameters"]["label_period_candles"])
# .rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
# .min()
# )
return df
return dataframe
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# All indicators must be populated by populate_any_indicators() for live functionality
# to work correctly.
# All indicators must be populated by feature_engineering_*() functions
# the model will return all labels created by user in `populate_any_indicators`
# the model will return all labels created by user in `feature_engineering_*`
# (& appended targets), an indication of whether or not the prediction should be accepted,
# the target mean/std values for each of the labels created by user in
# `populate_any_indicators()` for each training period.
# `set_freqai_targets()` for each training period.
dataframe = self.freqai.start(dataframe, metadata, self)
for val in self.std_dev_multiplier_buy.range:
dataframe[f'target_roi_{val}'] = (
dataframe["&-s_close_mean"] + dataframe["&-s_close_std"] * val

View File

@@ -41,20 +41,6 @@
"pairlists": [
{{ '{"method": "StaticPairList"}' if exchange_name == 'bittrex' else volume_pairlist }}
],
"edge": {
"enabled": false,
"process_throttle_secs": 3600,
"calculate_since_number_of_days": 7,
"allowed_risk": 0.01,
"stoploss_range_min": -0.01,
"stoploss_range_max": -0.1,
"stoploss_range_step": -0.01,
"minimum_winrate": 0.60,
"minimum_expectancy": 0.20,
"min_trade_number": 10,
"max_trade_duration_minute": 1440,
"remove_pumps": false
},
"telegram": {
"enabled": {{ telegram | lower }},
"token": "{{ telegram_token }}",

View File

@@ -118,6 +118,7 @@
"from freqtrade.data.dataprovider import DataProvider\n",
"strategy = StrategyResolver.load_strategy(config)\n",
"strategy.dp = DataProvider(config, None, None)\n",
"strategy.ft_bot_start()\n",
"\n",
"# Generate buy/sell signals using strategy\n",
"df = strategy.analyze_ticker(candles, {'pair': pair})\n",

View File

@@ -0,0 +1,78 @@
import logging
from packaging import version
from freqtrade.constants import Config
from freqtrade.enums.tradingmode import TradingMode
from freqtrade.exceptions import OperationalException
from freqtrade.persistence.pairlock import PairLock
from freqtrade.persistence.trade_model import Trade
logger = logging.getLogger(__name__)
def migrate_binance_futures_names(config: Config):
if (
not (config.get('trading_mode', TradingMode.SPOT) == TradingMode.FUTURES
and config['exchange']['name'] == 'binance')
):
# only act on new futures
return
import ccxt
if version.parse("2.6.26") > version.parse(ccxt.__version__):
raise OperationalException(
"Please follow the update instructions in the docs "
"(https://www.freqtrade.io/en/latest/updating/) to install a compatible ccxt version.")
_migrate_binance_futures_db(config)
migrate_binance_futures_data(config)
def _migrate_binance_futures_db(config: Config):
logger.warning('Migrating binance futures pairs in database.')
trades = Trade.get_trades([Trade.exchange == 'binance', Trade.trading_mode == 'FUTURES']).all()
for trade in trades:
if ':' in trade.pair:
# already migrated
continue
new_pair = f"{trade.pair}:{trade.stake_currency}"
trade.pair = new_pair
for order in trade.orders:
order.ft_pair = new_pair
# Should symbol be migrated too?
# order.symbol = new_pair
Trade.commit()
pls = PairLock.query.filter(PairLock.pair.notlike('%:%'))
for pl in pls:
pl.pair = f"{pl.pair}:{config['stake_currency']}"
# print(pls)
# pls.update({'pair': concat(PairLock.pair,':USDT')})
Trade.commit()
logger.warning('Done migrating binance futures pairs in database.')
def migrate_binance_futures_data(config: Config):
if (
not (config.get('trading_mode', TradingMode.SPOT) == TradingMode.FUTURES
and config['exchange']['name'] == 'binance')
):
# only act on new futures
return
from freqtrade.data.history.idatahandler import get_datahandler
dhc = get_datahandler(config['datadir'], config.get('dataformat_ohlcv', 'json'))
paircombs = dhc.ohlcv_get_available_data(
config['datadir'],
config.get('trading_mode', TradingMode.SPOT)
)
for pair, timeframe, candle_type in paircombs:
if ':' in pair:
# already migrated
continue
new_pair = f"{pair}:{config['stake_currency']}"
dhc.rename_futures_data(pair, new_pair, timeframe, candle_type)

View File

@@ -291,17 +291,22 @@ class Wallets:
return self._check_available_stake_amount(stake_amount, available_amount)
def validate_stake_amount(self, pair: str, stake_amount: Optional[float],
min_stake_amount: Optional[float], max_stake_amount: float):
min_stake_amount: Optional[float], max_stake_amount: float,
trade_amount: Optional[float]):
if not stake_amount:
logger.debug(f"Stake amount is {stake_amount}, ignoring possible trade for {pair}.")
return 0
max_stake_amount = min(max_stake_amount, self.get_available_stake_amount())
max_allowed_stake = min(max_stake_amount, self.get_available_stake_amount())
if trade_amount:
# if in a trade, then the resulting trade size cannot go beyond the max stake
# Otherwise we could no longer exit.
max_allowed_stake = min(max_allowed_stake, max_stake_amount - trade_amount)
if min_stake_amount is not None and min_stake_amount > max_stake_amount:
if min_stake_amount is not None and min_stake_amount > max_allowed_stake:
if self._log:
logger.warning("Minimum stake amount > available balance. "
f"{min_stake_amount} > {max_stake_amount}")
f"{min_stake_amount} > {max_allowed_stake}")
return 0
if min_stake_amount is not None and stake_amount < min_stake_amount:
if self._log:
@@ -320,11 +325,11 @@ class Wallets:
return 0
stake_amount = min_stake_amount
if stake_amount > max_stake_amount:
if stake_amount > max_allowed_stake:
if self._log:
logger.info(
f"Stake amount for pair {pair} is too big "
f"({stake_amount} > {max_stake_amount}), adjusting to {max_stake_amount}."
f"({stake_amount} > {max_allowed_stake}), adjusting to {max_allowed_stake}."
)
stake_amount = max_stake_amount
stake_amount = max_allowed_stake
return stake_amount

View File

@@ -26,7 +26,7 @@ class Worker:
Freqtradebot worker class
"""
def __init__(self, args: Dict[str, Any], config: Config = None) -> None:
def __init__(self, args: Dict[str, Any], config: Optional[Config] = None) -> None:
"""
Init all variables and objects the bot needs to work
"""