Merge branch 'freqtrade:develop' into strategy_utils
This commit is contained in:
@@ -1,19 +1,20 @@
|
||||
""" Freqtrade bot """
|
||||
__version__ = '2023.1.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,
|
||||
|
@@ -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,6 +25,7 @@ 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',
|
||||
@@ -229,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}
|
||||
@@ -636,7 +638,6 @@ SCHEMA_TRADE_REQUIRED = [
|
||||
|
||||
SCHEMA_BACKTEST_REQUIRED = [
|
||||
'exchange',
|
||||
'max_open_trades',
|
||||
'stake_currency',
|
||||
'stake_amount',
|
||||
'dry_run_wallet',
|
||||
@@ -646,6 +647,7 @@ SCHEMA_BACKTEST_REQUIRED = [
|
||||
SCHEMA_BACKTEST_REQUIRED_FINAL = SCHEMA_BACKTEST_REQUIRED + [
|
||||
'stoploss',
|
||||
'minimal_roi',
|
||||
'max_open_trades'
|
||||
]
|
||||
|
||||
SCHEMA_MINIMAL_REQUIRED = [
|
||||
@@ -681,3 +683,4 @@ MakerTaker = Literal['maker', 'taker']
|
||||
BidAsk = Literal['bid', 'ask']
|
||||
|
||||
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
|
||||
@@ -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.
|
||||
@@ -332,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,7 +9,7 @@ from collections import deque
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from pandas import DataFrame, to_timedelta
|
||||
from pandas import DataFrame, Timedelta, Timestamp, to_timedelta
|
||||
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.constants import (FULL_DATAFRAME_THRESHOLD, Config, ListPairsWithTimeframes,
|
||||
@@ -206,9 +206,11 @@ class DataProvider:
|
||||
existing_df, _ = self.__producer_pairs_df[producer_name][pair_key]
|
||||
|
||||
# CHECK FOR MISSING CANDLES
|
||||
timeframe_delta = to_timedelta(timeframe) # Convert the timeframe to a timedelta for pandas
|
||||
local_last = existing_df.iloc[-1]['date'] # We want the last date from our copy
|
||||
incoming_first = dataframe.iloc[0]['date'] # We want the first date from the incoming
|
||||
# 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]
|
||||
@@ -221,7 +223,7 @@ class DataProvider:
|
||||
# we missed some candles between our data and the incoming
|
||||
# so return False and candle_difference.
|
||||
if candle_difference > 1:
|
||||
return (False, candle_difference)
|
||||
return (False, int(candle_difference))
|
||||
if existing_df1.empty:
|
||||
appended_df = dataframe
|
||||
else:
|
||||
@@ -281,7 +283,7 @@ class DataProvider:
|
||||
def historic_ohlcv(
|
||||
self,
|
||||
pair: str,
|
||||
timeframe: str = None,
|
||||
timeframe: Optional[str] = None,
|
||||
candle_type: str = ''
|
||||
) -> DataFrame:
|
||||
"""
|
||||
@@ -333,7 +335,7 @@ class DataProvider:
|
||||
def get_pair_dataframe(
|
||||
self,
|
||||
pair: str,
|
||||
timeframe: str = None,
|
||||
timeframe: Optional[str] = None,
|
||||
candle_type: str = ''
|
||||
) -> DataFrame:
|
||||
"""
|
||||
@@ -415,7 +417,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
|
||||
"""
|
||||
@@ -439,7 +441,7 @@ class DataProvider:
|
||||
def ohlcv(
|
||||
self,
|
||||
pair: str,
|
||||
timeframe: str = None,
|
||||
timeframe: Optional[str] = None,
|
||||
copy: bool = True,
|
||||
candle_type: str = ''
|
||||
) -> DataFrame:
|
||||
|
@@ -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
|
||||
|
@@ -197,7 +197,7 @@ def calculate_cagr(days_passed: int, starting_balance: float, final_balance: flo
|
||||
def calculate_expectancy(trades: pd.DataFrame) -> float:
|
||||
"""
|
||||
Calculate expectancy
|
||||
:param trades: DataFrame containing trades (requires columns close_date and profit_ratio)
|
||||
:param trades: DataFrame containing trades (requires columns close_date and profit_abs)
|
||||
:return: expectancy
|
||||
"""
|
||||
if len(trades) == 0:
|
||||
|
@@ -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"
|
@@ -17,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
|
||||
|
@@ -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": "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
|
||||
|
||||
@@ -24,6 +24,7 @@ from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHAN
|
||||
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, 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
|
||||
"""
|
||||
@@ -1167,6 +1177,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 +1371,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.
|
||||
@@ -1705,7 +1719,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 +1815,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 +1827,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 +1869,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 +1915,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 +1979,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 +1996,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 +2026,7 @@ class Exchange:
|
||||
timeframe: str,
|
||||
candle_type: CandleType,
|
||||
since_ms: Optional[int] = None,
|
||||
) -> Tuple[str, str, str, List]:
|
||||
) -> OHLCVResponse:
|
||||
"""
|
||||
Asynchronously get candle history data using fetch_ohlcv
|
||||
:param candle_type: '', mark, index, premiumIndex, or funding_rate
|
||||
@@ -2065,9 +2066,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(
|
||||
@@ -2504,7 +2505,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
|
||||
@@ -2513,12 +2515,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
|
||||
@@ -2540,7 +2548,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")
|
||||
@@ -2554,6 +2563,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
|
||||
@@ -2687,7 +2700,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']:
|
||||
@@ -2707,6 +2720,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
|
||||
@@ -2728,6 +2742,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,
|
||||
@@ -2739,7 +2754,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
|
||||
@@ -2757,6 +2772,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
|
||||
@@ -2764,7 +2780,7 @@ class Exchange:
|
||||
"""
|
||||
Important: Must be fetching data from cached values as this is used by backtesting!
|
||||
PERPETUAL:
|
||||
gateio: https://www.gate.io/help/futures/futures/27724/liquidation-price-bankruptcy-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:
|
||||
@@ -2778,13 +2794,14 @@ class Exchange:
|
||||
: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:
|
||||
"""
|
||||
|
@@ -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
|
@@ -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):
|
||||
@@ -14,3 +16,6 @@ class Ticker(TypedDict):
|
||||
|
||||
|
||||
Tickers = Dict[str, Ticker]
|
||||
|
||||
# pair, timeframe, candleType, OHLCV, drop last?,
|
||||
OHLCVResponse = Tuple[str, str, CandleType, List, bool]
|
||||
|
@@ -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, can_short: bool = False):
|
||||
fee: float = 0.0015, can_short: bool = False, pair: str = "",
|
||||
df_raw: DataFrame = DataFrame()):
|
||||
"""
|
||||
Initializes the training/eval environment.
|
||||
:param df: dataframe of features
|
||||
@@ -60,12 +61,14 @@ class BaseEnvironment(gym.Env):
|
||||
: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:
|
||||
@@ -74,8 +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.can_short = can_short
|
||||
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.")
|
||||
@@ -93,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,7 +161,7 @@ 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
|
||||
"""
|
||||
@@ -166,7 +169,9 @@ class BaseReinforcementLearningModel(IFreqaiModel):
|
||||
"reward_kwargs": self.reward_params,
|
||||
"config": self.config,
|
||||
"live": self.live,
|
||||
"can_short": self.can_short}
|
||||
"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
|
||||
@@ -280,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)
|
||||
|
||||
@@ -337,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.
|
||||
@@ -353,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,10 +1,11 @@
|
||||
import copy
|
||||
import inspect
|
||||
import logging
|
||||
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
|
||||
@@ -23,6 +24,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
|
||||
|
||||
|
||||
@@ -110,7 +112,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
|
||||
@@ -1145,9 +1147,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)
|
||||
@@ -1176,6 +1178,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,
|
||||
@@ -1188,7 +1289,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
|
||||
@@ -106,6 +106,8 @@ class IFreqaiModel(ABC):
|
||||
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)
|
||||
|
||||
def __getstate__(self):
|
||||
@@ -136,6 +138,9 @@ class IFreqaiModel(ABC):
|
||||
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')
|
||||
self.dk = FreqaiDataKitchen(self.config, self.live, metadata["pair"])
|
||||
@@ -147,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(
|
||||
@@ -255,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
|
||||
@@ -267,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)
|
||||
@@ -301,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:
|
||||
@@ -350,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)
|
||||
@@ -913,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')
|
||||
@@ -1094,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)
|
||||
@@ -1156,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.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.
|
||||
@@ -1249,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()
|
||||
@@ -1261,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:
|
||||
@@ -1518,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
|
||||
@@ -1595,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']
|
||||
@@ -1612,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.
|
||||
"""
|
||||
@@ -1622,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)
|
||||
@@ -1725,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.
|
||||
@@ -1784,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')
|
||||
|
@@ -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,
|
||||
@@ -779,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
|
||||
@@ -804,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
|
||||
@@ -867,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,
|
||||
))
|
||||
@@ -920,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
|
||||
@@ -1051,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.
|
||||
|
||||
@@ -1070,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)
|
||||
@@ -1099,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)
|
||||
@@ -1121,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
|
||||
|
||||
@@ -1134,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)
|
||||
@@ -1164,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]
|
||||
@@ -1175,8 +1184,8 @@ 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]
|
||||
@@ -1188,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()
|
||||
@@ -1226,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)
|
||||
@@ -1255,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,7 +8,7 @@ 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)
|
||||
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)
|
||||
@@ -191,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
|
||||
|
@@ -214,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}
|
||||
"""))
|
||||
|
||||
@@ -311,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_orders, 'funding_fee'):
|
||||
if not has_column(cols_trades, 'max_stake_amount'):
|
||||
# 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
|
||||
@@ -784,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.
|
||||
@@ -822,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.
|
||||
@@ -843,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.
|
||||
@@ -956,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
|
||||
@@ -968,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:
|
||||
@@ -1041,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.
|
||||
@@ -1156,44 +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)
|
||||
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_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)
|
||||
@@ -1201,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)
|
||||
@@ -1239,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
|
||||
|
@@ -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
|
||||
|
@@ -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]
|
||||
@@ -422,7 +423,7 @@ class BacktestRequest(BaseModel):
|
||||
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]
|
||||
|
@@ -40,7 +40,9 @@ logger = logging.getLogger(__name__)
|
||||
# 2.20: Add websocket endpoints
|
||||
# 2.21: Add new_candle messagetype
|
||||
# 2.22: Add FreqAI to backtesting
|
||||
API_VERSION = 2.22
|
||||
# 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()
|
||||
@@ -122,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)):
|
||||
@@ -248,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'])
|
||||
|
@@ -90,7 +90,7 @@ 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
|
||||
|
@@ -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'),
|
||||
@@ -673,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.'}
|
||||
|
||||
@@ -811,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>.
|
||||
@@ -944,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:
|
||||
@@ -1126,12 +1151,12 @@ class RPC:
|
||||
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,
|
||||
@@ -1156,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))
|
||||
|
@@ -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
|
||||
@@ -1053,8 +1155,8 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
|
||||
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:
|
||||
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
|
||||
|
@@ -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'].
|
||||
"""
|
||||
|
||||
@@ -40,134 +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
|
||||
# freqai/prediction_models/CatboostRegressorMultiTarget.py,
|
||||
# freqtrade trade --freqaimodel CatboostRegressorMultiTarget
|
||||
|
||||
# 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)
|
@@ -297,16 +297,16 @@ class Wallets:
|
||||
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_stake_amount = min(max_stake_amount, max_stake_amount - trade_amount)
|
||||
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:
|
||||
@@ -325,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