freqtrade_origin/freqtrade/edge/edge_positioning.py

525 lines
20 KiB
Python
Raw Normal View History

2020-01-26 09:44:42 +00:00
# pragma pylint: disable=W0603
2024-05-12 14:24:43 +00:00
"""Edge positioning package"""
2020-01-26 09:44:42 +00:00
import logging
from collections import defaultdict
from copy import deepcopy
2023-05-14 08:34:06 +00:00
from datetime import timedelta
2020-02-02 04:00:40 +00:00
from typing import Any, Dict, List, NamedTuple
2020-01-26 09:44:42 +00:00
import numpy as np
import utils_find_1st as utf1st
from pandas import DataFrame
from freqtrade.configuration import TimeRange
2022-09-18 11:20:36 +00:00
from freqtrade.constants import DATETIME_PRINT_FORMAT, UNLIMITED_STAKE_AMOUNT, Config
2020-03-19 23:10:44 +00:00
from freqtrade.data.history import get_timerange, load_data, refresh_data
2022-03-25 06:00:35 +00:00
from freqtrade.enums import CandleType, ExitType, RunMode
2020-09-28 17:39:41 +00:00
from freqtrade.exceptions import OperationalException
2022-08-25 05:08:58 +00:00
from freqtrade.exchange import timeframe_to_seconds
2021-01-12 00:13:58 +00:00
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
2021-06-08 19:04:34 +00:00
from freqtrade.strategy.interface import IStrategy
2023-05-14 08:34:06 +00:00
from freqtrade.util import dt_now
2020-01-26 09:44:42 +00:00
2020-09-28 17:39:41 +00:00
2020-01-26 09:44:42 +00:00
logger = logging.getLogger(__name__)
class PairInfo(NamedTuple):
stoploss: float
winrate: float
risk_reward_ratio: float
required_risk_reward: float
expectancy: float
nb_trades: int
avg_trade_duration: float
class Edge:
"""
Calculates Win Rate, Risk Reward Ratio, Expectancy
against historical data for a give set of markets and a strategy
it then adjusts stoploss and position size accordingly
and force it into the strategy
Author: https://github.com/mishaker
"""
_cached_pairs: Dict[str, Any] = {} # Keeps a list of pairs
2022-09-18 11:20:36 +00:00
def __init__(self, config: Config, exchange, strategy) -> None:
2020-01-26 09:44:42 +00:00
self.config = config
self.exchange = exchange
self.strategy: IStrategy = strategy
2020-01-26 09:44:42 +00:00
2024-05-12 14:24:43 +00:00
self.edge_config = self.config.get("edge", {})
2020-01-26 09:44:42 +00:00
self._cached_pairs: Dict[str, Any] = {} # Keeps a list of pairs
self._final_pairs: list = []
# checking max_open_trades. it should be -1 as with Edge
# the number of trades is determined by position size
2024-05-12 14:24:43 +00:00
if self.config["max_open_trades"] != float("inf"):
logger.critical("max_open_trades should be -1 in config !")
2020-01-26 09:44:42 +00:00
2024-05-12 14:24:43 +00:00
if self.config["stake_amount"] != UNLIMITED_STAKE_AMOUNT:
raise OperationalException("Edge works only with unlimited stake amount")
2020-01-26 09:44:42 +00:00
2024-05-12 14:24:43 +00:00
self._capital_ratio: float = self.config["tradable_balance_ratio"]
self._allowed_risk: float = self.edge_config.get("allowed_risk")
self._since_number_of_days: int = self.edge_config.get("calculate_since_number_of_days", 14)
2020-01-26 09:44:42 +00:00
self._last_updated: int = 0 # Timestamp of pairs last updated time
self._refresh_pairs = True
2024-05-12 14:24:43 +00:00
self._stoploss_range_min = float(self.edge_config.get("stoploss_range_min", -0.01))
self._stoploss_range_max = float(self.edge_config.get("stoploss_range_max", -0.05))
self._stoploss_range_step = float(self.edge_config.get("stoploss_range_step", -0.001))
2020-01-26 09:44:42 +00:00
# calculating stoploss range
self._stoploss_range = np.arange(
2024-05-12 14:24:43 +00:00
self._stoploss_range_min, self._stoploss_range_max, self._stoploss_range_step
2020-01-26 09:44:42 +00:00
)
2023-05-14 09:12:32 +00:00
self._timerange: TimeRange = TimeRange.parse_timerange(
2024-05-12 14:24:43 +00:00
f"{(dt_now() - timedelta(days=self._since_number_of_days)).strftime('%Y%m%d')}-"
)
if config.get("fee"):
self.fee = config["fee"]
2020-01-26 09:44:42 +00:00
else:
try:
2024-05-12 14:24:43 +00:00
self.fee = self.exchange.get_fee(
symbol=expand_pairlist(
self.config["exchange"]["pair_whitelist"], list(self.exchange.markets)
)[0]
)
except IndexError:
self.fee = None
2020-01-26 09:44:42 +00:00
def calculate(self, pairs: List[str]) -> bool:
if self.fee is None and pairs:
self.fee = self.exchange.get_fee(pairs[0])
2024-05-12 14:24:43 +00:00
heartbeat = self.edge_config.get("process_throttle_secs")
2020-01-26 09:44:42 +00:00
if (self._last_updated > 0) and (
2024-05-12 14:24:43 +00:00
self._last_updated + heartbeat > int(dt_now().timestamp())
):
2020-01-26 09:44:42 +00:00
return False
data: Dict[str, Any] = {}
2024-05-12 14:24:43 +00:00
logger.info("Using stake_currency: %s ...", self.config["stake_currency"])
logger.info("Using local backtesting data (using whitelist in given config) ...")
2020-01-26 09:44:42 +00:00
if self._refresh_pairs:
timerange_startup = deepcopy(self._timerange)
2024-05-12 14:24:43 +00:00
timerange_startup.subtract_start(
timeframe_to_seconds(self.strategy.timeframe) * self.strategy.startup_candle_count
)
2020-03-19 23:10:44 +00:00
refresh_data(
2024-05-12 14:24:43 +00:00
datadir=self.config["datadir"],
2020-01-26 09:44:42 +00:00
pairs=pairs,
exchange=self.exchange,
2020-06-02 07:36:04 +00:00
timeframe=self.strategy.timeframe,
timerange=timerange_startup,
2024-05-12 14:24:43 +00:00
data_format=self.config["dataformat_ohlcv"],
candle_type=self.config.get("candle_type_def", CandleType.SPOT),
2020-01-26 09:44:42 +00:00
)
# Download informative pairs too
res = defaultdict(list)
for pair, timeframe, _ in self.strategy.gather_informative_pairs():
res[timeframe].append(pair)
for timeframe, inf_pairs in res.items():
timerange_startup = deepcopy(self._timerange)
2024-05-12 14:24:43 +00:00
timerange_startup.subtract_start(
timeframe_to_seconds(timeframe) * self.strategy.startup_candle_count
)
refresh_data(
2024-05-12 14:24:43 +00:00
datadir=self.config["datadir"],
pairs=inf_pairs,
exchange=self.exchange,
timeframe=timeframe,
timerange=timerange_startup,
2024-05-12 14:24:43 +00:00
data_format=self.config["dataformat_ohlcv"],
candle_type=self.config.get("candle_type_def", CandleType.SPOT),
)
2020-01-26 09:44:42 +00:00
2020-03-19 23:10:44 +00:00
data = load_data(
2024-05-12 14:24:43 +00:00
datadir=self.config["datadir"],
2020-01-26 09:44:42 +00:00
pairs=pairs,
2020-06-02 07:36:04 +00:00
timeframe=self.strategy.timeframe,
2020-01-26 09:44:42 +00:00
timerange=self._timerange,
startup_candles=self.strategy.startup_candle_count,
2024-05-12 14:24:43 +00:00
data_format=self.config["dataformat_ohlcv"],
candle_type=self.config.get("candle_type_def", CandleType.SPOT),
2020-01-26 09:44:42 +00:00
)
if not data:
# Reinitializing cached pairs
self._cached_pairs = {}
logger.critical("No data found. Edge is stopped ...")
return False
# Fake run-mode to Edge
2024-05-12 14:24:43 +00:00
prior_rm = self.config["runmode"]
self.config["runmode"] = RunMode.EDGE
preprocessed = self.strategy.advise_all_indicators(data)
2024-05-12 14:24:43 +00:00
self.config["runmode"] = prior_rm
2020-01-26 09:44:42 +00:00
# Print timeframe
2020-03-19 23:10:44 +00:00
min_date, max_date = get_timerange(preprocessed)
2024-05-12 14:24:43 +00:00
logger.info(
f"Measuring data from {min_date.strftime(DATETIME_PRINT_FORMAT)} "
f"up to {max_date.strftime(DATETIME_PRINT_FORMAT)} "
f"({(max_date - min_date).days} days).."
)
# TODO: Should edge support shorts? needs to be investigated further
2022-02-12 05:05:46 +00:00
# * (add enter_short exit_short)
2024-05-12 14:24:43 +00:00
headers = ["date", "open", "high", "low", "close", "enter_long", "exit_long"]
2020-01-26 09:44:42 +00:00
trades: list = []
for pair, pair_data in preprocessed.items():
# Sorting dataframe by date and reset index
2024-05-12 14:24:43 +00:00
pair_data = pair_data.sort_values(by=["date"])
2020-01-26 09:44:42 +00:00
pair_data = pair_data.reset_index(drop=True)
2024-05-12 14:24:43 +00:00
df_analyzed = self.strategy.ft_advise_signals(pair_data, {"pair": pair})[headers].copy()
2020-01-26 09:44:42 +00:00
trades += self._find_trades_for_stoploss_range(df_analyzed, pair, self._stoploss_range)
2020-01-26 09:44:42 +00:00
# If no trade found then exit
if len(trades) == 0:
logger.info("No trades found.")
return False
# Fill missing, calculable columns, profit, duration , abs etc.
trades_df = self._fill_calculable_fields(DataFrame(trades))
self._cached_pairs = self._process_expectancy(trades_df)
2023-05-14 08:34:06 +00:00
self._last_updated = int(dt_now().timestamp())
2020-01-26 09:44:42 +00:00
return True
2024-05-12 14:24:43 +00:00
def stake_amount(
self, pair: str, free_capital: float, total_capital: float, capital_in_trade: float
) -> float:
stoploss = self.get_stoploss(pair)
available_capital = (total_capital + capital_in_trade) * self._capital_ratio
2020-01-26 09:44:42 +00:00
allowed_capital_at_risk = available_capital * self._allowed_risk
max_position_size = abs(allowed_capital_at_risk / stoploss)
# Position size must be below available capital.
position_size = min(min(max_position_size, free_capital), available_capital)
2020-01-26 09:44:42 +00:00
if pair in self._cached_pairs:
logger.info(
2024-05-12 14:24:43 +00:00
"winrate: %s, expectancy: %s, position size: %s, pair: %s,"
" capital in trade: %s, free capital: %s, total capital: %s,"
" stoploss: %s, available capital: %s.",
2020-01-26 09:44:42 +00:00
self._cached_pairs[pair].winrate,
self._cached_pairs[pair].expectancy,
2024-05-12 14:24:43 +00:00
position_size,
pair,
capital_in_trade,
free_capital,
total_capital,
stoploss,
available_capital,
2020-01-26 09:44:42 +00:00
)
return round(position_size, 15)
def get_stoploss(self, pair: str) -> float:
2020-01-26 09:44:42 +00:00
if pair in self._cached_pairs:
return self._cached_pairs[pair].stoploss
else:
2024-05-12 14:24:43 +00:00
logger.warning(
f"Tried to access stoploss of non-existing pair {pair}, "
"strategy stoploss is returned instead."
)
2020-01-26 09:44:42 +00:00
return self.strategy.stoploss
2020-02-02 04:00:40 +00:00
def adjust(self, pairs: List[str]) -> list:
2020-01-26 09:44:42 +00:00
"""
Filters out and sorts "pairs" according to Edge calculated pairs
"""
final = []
for pair, info in self._cached_pairs.items():
2022-03-17 06:41:08 +00:00
if (
2024-05-12 14:24:43 +00:00
info.expectancy > float(self.edge_config.get("minimum_expectancy", 0.2))
and info.winrate > float(self.edge_config.get("minimum_winrate", 0.60))
2022-03-17 06:41:08 +00:00
and pair in pairs
):
2020-01-26 09:44:42 +00:00
final.append(pair)
if self._final_pairs != final:
self._final_pairs = final
if self._final_pairs:
logger.info(
2024-05-12 14:24:43 +00:00
"Minimum expectancy and minimum winrate are met only for %s,"
" so other pairs are filtered out.",
self._final_pairs,
2021-08-06 22:19:36 +00:00
)
2020-01-26 09:44:42 +00:00
else:
logger.info(
2024-05-12 14:24:43 +00:00
"Edge removed all pairs as no pair with minimum expectancy "
"and minimum winrate was found !"
2021-08-06 22:19:36 +00:00
)
2020-01-26 09:44:42 +00:00
return self._final_pairs
def accepted_pairs(self) -> List[Dict[str, Any]]:
2020-01-26 09:44:42 +00:00
"""
return a list of accepted pairs along with their winrate, expectancy and stoploss
"""
final = []
for pair, info in self._cached_pairs.items():
2024-05-12 14:24:43 +00:00
if info.expectancy > float(
self.edge_config.get("minimum_expectancy", 0.2)
) and info.winrate > float(self.edge_config.get("minimum_winrate", 0.60)):
final.append(
{
"Pair": pair,
"Winrate": info.winrate,
"Expectancy": info.expectancy,
"Stoploss": info.stoploss,
}
)
2020-01-26 09:44:42 +00:00
return final
def _fill_calculable_fields(self, result: DataFrame) -> DataFrame:
"""
The result frame contains a number of columns that are calculable
from other columns. These are left blank till all rows are added,
to be populated in single vector calls.
Columns to be populated are:
- Profit
- trade duration
- profit abs
:param result Dataframe
:return: result Dataframe
"""
2020-05-09 23:22:16 +00:00
# We set stake amount to an arbitrary amount, as it doesn't change the calculation.
# All returned values are relative, they are defined as ratios.
2020-01-26 09:44:42 +00:00
stake = 0.015
2020-05-09 23:22:16 +00:00
2024-05-12 14:24:43 +00:00
result["trade_duration"] = result["close_date"] - result["open_date"]
2020-01-26 09:44:42 +00:00
2024-05-12 14:24:43 +00:00
result["trade_duration"] = result["trade_duration"].map(
lambda x: int(x.total_seconds() / 60)
)
2020-01-26 09:44:42 +00:00
# Spends, Takes, Profit, Absolute Profit
# Buy Price
2024-05-12 14:24:43 +00:00
result["buy_vol"] = stake / result["open_rate"] # How many target are we buying
result["buy_fee"] = stake * self.fee
result["buy_spend"] = stake + result["buy_fee"] # How much we're spending
2020-01-26 09:44:42 +00:00
# Sell price
2024-05-12 14:24:43 +00:00
result["sell_sum"] = result["buy_vol"] * result["close_rate"]
result["sell_fee"] = result["sell_sum"] * self.fee
result["sell_take"] = result["sell_sum"] - result["sell_fee"]
2020-01-26 09:44:42 +00:00
2020-02-28 09:36:39 +00:00
# profit_ratio
2024-05-12 14:24:43 +00:00
result["profit_ratio"] = (result["sell_take"] - result["buy_spend"]) / result["buy_spend"]
2020-01-26 09:44:42 +00:00
# Absolute profit
2024-05-12 14:24:43 +00:00
result["profit_abs"] = result["sell_take"] - result["buy_spend"]
2020-01-26 09:44:42 +00:00
return result
def _process_expectancy(self, results: DataFrame) -> Dict[str, Any]:
"""
This calculates WinRate, Required Risk Reward, Risk Reward and Expectancy of all pairs
2021-06-25 13:45:49 +00:00
The calculation will be done per pair and per strategy.
2020-01-26 09:44:42 +00:00
"""
# Removing pairs having less than min_trades_number
2024-05-12 14:24:43 +00:00
min_trades_number = self.edge_config.get("min_trade_number", 10)
results = results.groupby(["pair", "stoploss"]).filter(lambda x: len(x) > min_trades_number)
2020-01-26 09:44:42 +00:00
###################################
# Removing outliers (Only Pumps) from the dataset
# The method to detect outliers is to calculate standard deviation
# Then every value more than (standard deviation + 2*average) is out (pump)
#
# Removing Pumps
2024-05-12 14:24:43 +00:00
if self.edge_config.get("remove_pumps", False):
results = results[
results["profit_abs"]
< 2 * results["profit_abs"].std() + results["profit_abs"].mean()
]
2020-01-26 09:44:42 +00:00
##########################################################################
# Removing trades having a duration more than X minutes (set in config)
2024-05-12 14:24:43 +00:00
max_trade_duration = self.edge_config.get("max_trade_duration_minute", 1440)
2020-01-26 09:44:42 +00:00
results = results[results.trade_duration < max_trade_duration]
#######################################################################
if results.empty:
return {}
groupby_aggregator = {
2024-05-12 14:24:43 +00:00
"profit_abs": [
("nb_trades", "count"), # number of all trades
("profit_sum", lambda x: x[x > 0].sum()), # cumulative profit of all winning trades
("loss_sum", lambda x: abs(x[x < 0].sum())), # cumulative loss of all losing trades
("nb_win_trades", lambda x: x[x > 0].count()), # number of winning trades
2020-01-26 09:44:42 +00:00
],
2024-05-12 14:24:43 +00:00
"trade_duration": [("avg_trade_duration", "mean")],
2020-01-26 09:44:42 +00:00
}
# Group by (pair and stoploss) by applying above aggregator
2024-05-12 14:24:43 +00:00
df = (
results.groupby(["pair", "stoploss"])[["profit_abs", "trade_duration"]]
.agg(groupby_aggregator)
.reset_index(col_level=1)
)
2020-01-26 09:44:42 +00:00
# Dropping level 0 as we don't need it
df.columns = df.columns.droplevel(0)
# Calculating number of losing trades, average win and average loss
2024-05-12 14:24:43 +00:00
df["nb_loss_trades"] = df["nb_trades"] - df["nb_win_trades"]
df["average_win"] = np.where(
df["nb_win_trades"] == 0, 0.0, df["profit_sum"] / df["nb_win_trades"]
)
df["average_loss"] = np.where(
df["nb_loss_trades"] == 0, 0.0, df["loss_sum"] / df["nb_loss_trades"]
)
2020-01-26 09:44:42 +00:00
# Win rate = number of profitable trades / number of trades
2024-05-12 14:24:43 +00:00
df["winrate"] = df["nb_win_trades"] / df["nb_trades"]
2020-01-26 09:44:42 +00:00
# risk_reward_ratio = average win / average loss
2024-05-12 14:24:43 +00:00
df["risk_reward_ratio"] = df["average_win"] / df["average_loss"]
2020-01-26 09:44:42 +00:00
# required_risk_reward = (1 / winrate) - 1
2024-05-12 14:24:43 +00:00
df["required_risk_reward"] = (1 / df["winrate"]) - 1
2020-01-26 09:44:42 +00:00
# expectancy = (risk_reward_ratio * winrate) - (lossrate)
2024-05-12 14:24:43 +00:00
df["expectancy"] = (df["risk_reward_ratio"] * df["winrate"]) - (1 - df["winrate"])
2020-01-26 09:44:42 +00:00
# sort by expectancy and stoploss
2024-05-12 14:24:43 +00:00
df = (
df.sort_values(by=["expectancy", "stoploss"], ascending=False)
.groupby("pair")
.first()
.sort_values(by=["expectancy"], ascending=False)
.reset_index()
)
2020-01-26 09:44:42 +00:00
final = {}
for x in df.itertuples():
final[x.pair] = PairInfo(
x.stoploss,
x.winrate,
x.risk_reward_ratio,
x.required_risk_reward,
x.expectancy,
x.nb_trades,
2024-05-12 14:24:43 +00:00
x.avg_trade_duration,
2020-01-26 09:44:42 +00:00
)
# Returning a list of pairs in order of "expectancy"
return final
2022-11-14 19:56:35 +00:00
def _find_trades_for_stoploss_range(self, df, pair: str, stoploss_range) -> list:
2024-05-12 14:24:43 +00:00
buy_column = df["enter_long"].values
sell_column = df["exit_long"].values
date_column = df["date"].values
ohlc_columns = df[["open", "high", "low", "close"]].values
2020-01-26 09:44:42 +00:00
result: list = []
for stoploss in stoploss_range:
result += self._detect_next_stop_or_sell_point(
buy_column, sell_column, date_column, ohlc_columns, round(stoploss, 6), pair
)
return result
2024-05-12 14:24:43 +00:00
def _detect_next_stop_or_sell_point(
self, buy_column, sell_column, date_column, ohlc_columns, stoploss, pair: str
):
2020-01-26 09:44:42 +00:00
"""
Iterate through ohlc_columns in order to find the next trade
Next trade opens from the first buy signal noticed to
The sell or stoploss signal after it.
It then cuts OHLC, buy_column, sell_column and date_column.
Cut from (the exit trade index) + 1.
Author: https://github.com/mishaker
"""
result: list = []
start_point = 0
while True:
open_trade_index = utf1st.find_1st(buy_column, 1, utf1st.cmp_equal)
# Return empty if we don't find trade entry (i.e. buy==1) or
# we find a buy but at the end of array
if open_trade_index == -1 or open_trade_index == len(buy_column) - 1:
break
else:
# When a buy signal is seen,
# trade opens in reality on the next candle
open_trade_index += 1
open_price = ohlc_columns[open_trade_index, 0]
2024-05-12 14:24:43 +00:00
stop_price = open_price * (stoploss + 1)
2020-01-26 09:44:42 +00:00
# Searching for the index where stoploss is hit
stop_index = utf1st.find_1st(
2024-05-12 14:24:43 +00:00
ohlc_columns[open_trade_index:, 2], stop_price, utf1st.cmp_smaller
)
2020-01-26 09:44:42 +00:00
# If we don't find it then we assume stop_index will be far in future (infinite number)
if stop_index == -1:
2024-05-12 14:24:43 +00:00
stop_index = float("inf")
2020-01-26 09:44:42 +00:00
# Searching for the index where sell is hit
sell_index = utf1st.find_1st(sell_column[open_trade_index:], 1, utf1st.cmp_equal)
# If we don't find it then we assume sell_index will be far in future (infinite number)
if sell_index == -1:
2024-05-12 14:24:43 +00:00
sell_index = float("inf")
2020-01-26 09:44:42 +00:00
# Check if we don't find any stop or sell point (in that case trade remains open)
# It is not interesting for Edge to consider it so we simply ignore the trade
# And stop iterating there is no more entry
2024-05-12 14:24:43 +00:00
if stop_index == sell_index == float("inf"):
2020-01-26 09:44:42 +00:00
break
if stop_index <= sell_index:
exit_index = open_trade_index + stop_index
2022-03-25 05:55:37 +00:00
exit_type = ExitType.STOP_LOSS
2020-01-26 09:44:42 +00:00
exit_price = stop_price
elif stop_index > sell_index:
# If exit is SELL then we exit at the next candle
exit_index = open_trade_index + sell_index + 1
# Check if we have the next candle
if len(ohlc_columns) - 1 < exit_index:
break
2022-04-04 15:10:02 +00:00
exit_type = ExitType.EXIT_SIGNAL
2020-01-26 09:44:42 +00:00
exit_price = ohlc_columns[exit_index, 0]
2024-05-12 14:24:43 +00:00
trade = {
"pair": pair,
"stoploss": stoploss,
"profit_ratio": "",
"profit_abs": "",
"open_date": date_column[open_trade_index],
"close_date": date_column[exit_index],
"trade_duration": "",
"open_rate": round(open_price, 15),
"close_rate": round(exit_price, 15),
"exit_type": exit_type,
}
2020-01-26 09:44:42 +00:00
result.append(trade)
# Giving a view of exit_index till the end of array
buy_column = buy_column[exit_index:]
sell_column = sell_column[exit_index:]
date_column = date_column[exit_index:]
ohlc_columns = ohlc_columns[exit_index:]
start_point += exit_index
return result