merge develop into feat/shuffle_after_split
This commit is contained in:
@@ -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]}"
|
||||
|
@@ -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",
|
||||
|
@@ -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",
|
||||
|
@@ -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,
|
||||
|
@@ -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:
|
||||
|
@@ -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
|
||||
|
@@ -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()):
|
||||
|
@@ -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.
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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.
|
||||
|
@@ -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
|
||||
|
@@ -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]:
|
||||
"""
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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:
|
||||
|
@@ -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
|
||||
|
8
freqtrade/enums/pricetype.py
Normal file
8
freqtrade/enums/pricetype.py
Normal 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"
|
@@ -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
|
||||
|
@@ -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
|
@@ -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
@@ -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
|
||||
|
@@ -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',
|
||||
|
@@ -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!
|
||||
|
@@ -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")
|
||||
|
@@ -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
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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:
|
||||
|
@@ -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]
|
||||
|
125
freqtrade/freqai/RL/Base3ActionRLEnv.py
Normal file
125
freqtrade/freqai/RL/Base3ActionRLEnv.py
Normal 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
|
@@ -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)
|
||||
|
@@ -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:
|
||||
|
@@ -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
|
||||
|
@@ -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:
|
||||
|
@@ -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)
|
||||
|
@@ -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.
|
||||
|
||||
|
@@ -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,
|
||||
|
@@ -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,
|
||||
))
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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({
|
||||
|
@@ -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({
|
||||
|
@@ -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)
|
||||
|
@@ -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?
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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%}"),
|
||||
|
@@ -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}")
|
||||
|
@@ -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)
|
||||
|
||||
|
@@ -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,
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
206
freqtrade/plugins/pairlist/RemotePairList.py
Normal file
206
freqtrade/plugins/pairlist/RemotePairList.py
Normal 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
|
@@ -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:
|
||||
|
@@ -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)
|
||||
"""
|
||||
|
@@ -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:
|
||||
|
@@ -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
|
||||
|
@@ -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:
|
||||
|
@@ -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):
|
||||
|
@@ -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)):
|
||||
|
@@ -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))
|
||||
|
@@ -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):
|
||||
"""
|
||||
|
@@ -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 ----------------------------
|
||||
|
@@ -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}")
|
||||
|
@@ -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 {
|
||||
|
@@ -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:
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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:
|
||||
|
@@ -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
|
||||
|
@@ -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 }}",
|
||||
|
@@ -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",
|
||||
|
78
freqtrade/util/binance_mig.py
Normal file
78
freqtrade/util/binance_mig.py
Normal 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)
|
@@ -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
|
||||
|
@@ -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
|
||||
"""
|
||||
|
Reference in New Issue
Block a user