Timeframe
15m
Direction
Long Only
Stoploss
-99.0%
Trailing Stop
No
ROI
N/A
Interface Version
N/A
Startup Candles
200
Indicators
2
freqtrade/freqtrade-strategies
freqtrade/freqtrade-strategies
freqtrade/freqtrade-strategies
This strategy uses custom_stoploss() to enforce a fixed risk/reward ratio by first calculating a dynamic initial stoploss via ATR - last negative peak
freqtrade/freqtrade-strategies
import logging
from functools import reduce
import datetime
import talib.abstract as ta
import pandas_ta as pta
import logging
import numpy as np
import pandas as pd
import time
import freqtrade.vendor.qtpylib.indicators as qtpylib
from technical import qtpylib
from datetime import timedelta, datetime, timezone
from pandas import DataFrame, Series
from technical import qtpylib
from typing import Optional
from freqtrade.strategy.interface import IStrategy
from technical.pivots_points import pivots_points
from freqtrade.exchange import timeframe_to_prev_date, timeframe_to_minutes
from freqtrade.persistence import Trade
from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter,
IStrategy, IntParameter, RealParameter, merge_informative_pair)
from scipy.signal import argrelextrema
from typing import Optional, Union, Tuple
from functools import reduce
logger = logging.getLogger(__name__)
class PnF(IStrategy):
exit_profit_only = True ### No selling at a loss
use_custom_stoploss = False
trailing_stop = False
position_adjustment_enable = False
ignore_roi_if_entry_signal = True
max_entry_position_adjustment = 3
max_dca_multiplier = 3.5
process_only_new_candles = True
can_short = False
use_exit_signal = True
startup_candle_count = 200
stoploss = -0.99
timeframe = '15m'
locked_stoploss = {}
minimal_roi = {}
plot_config = {}
# DCA
position_adjustment_enable = True
# Example specific variables
max_entry_position_adjustment = 3
# This number is explained a bit further down
# max_dca_multiplier = 3
max_dca_multiplier = DecimalParameter(low=2.0, high=4.0, default=3, decimals=1 ,space='buy', optimize=True, load=True)
box_size = DecimalParameter(low=0.5, high=10.0, default=1.5, decimals=1 ,space='buy', optimize=True, load=True)
reversal = DecimalParameter(low=0.6, high=1.5, default=1.1, decimals=1 ,space='sell', optimize=True, load=True)
filldelay = IntParameter(10, 120, default = 17 ,space='buy', optimize=True, load=True)
roll = IntParameter(10, 40, default = 17 ,space='buy', optimize=True, load=True)
# protections
cooldown_lookback = IntParameter(12, 48, default=12, space="protection", optimize=True)
stop_duration = IntParameter(12, 48, default=12, space="protection", optimize=True)
use_stop_protection = BooleanParameter(default=True, space="protection", optimize=True)
### protections ###
@property
def protections(self):
prot = []
prot.append({
"method": "CooldownPeriod",
"stop_duration_candles": self.cooldown_lookback.value
})
if self.use_stop_protection.value:
prot.append({
"method": "StoplossGuard",
"lookback_period_candles": 24 * 3,
"trade_limit": 2,
"stop_duration_candles": self.stop_duration.value,
"only_per_pair": False
})
return prot
# This is called when placing the initial order (opening trade)
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
proposed_stake: float, min_stake: Optional[float], max_stake: float,
leverage: float, entry_tag: Optional[str], side: str,
**kwargs) -> float:
# We need to leave most of the funds for possible further DCA orders
# This also applies to fixed stakes
return proposed_stake / self.max_dca_multiplier.value
def adjust_trade_position(self, trade: Trade, current_time: datetime,
current_rate: float, current_profit: float,
min_stake: Optional[float], max_stake: float,
current_entry_rate: float, current_exit_rate: float,
current_entry_profit: float, current_exit_profit: float,
**kwargs
) -> Union[Optional[float], Tuple[Optional[float], Optional[str]]]:
"""
Custom trade adjustment logic, returning the stake amount that a trade should be
increased or decreased.
This means extra entry or exit orders with additional fees.
Only called when `position_adjustment_enable` is set to True.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
When not implemented by a strategy, returns None
:param trade: trade object.
:param current_time: datetime object, containing the current datetime
:param current_rate: Current entry rate (same as current_entry_profit)
:param current_profit: Current profit (as ratio), calculated based on current_rate
(same as current_entry_profit).
:param min_stake: Minimal stake size allowed by exchange (for both entries and exits)
:param max_stake: Maximum stake allowed (either through balance, or by exchange limits).
:param current_entry_rate: Current rate using entry pricing.
:param current_exit_rate: Current rate using exit pricing.
:param current_entry_profit: Current profit using entry pricing.
:param current_exit_profit: Current profit using exit pricing.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return float: Stake amount to adjust your trade,
Positive values to increase position, Negative values to decrease position.
Return None for no action.
Optionally, return a tuple with a 2nd element with an order reason
"""
dataframe, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe)
# Only buy when not actively falling price.
last_candle = dataframe.iloc[-1].squeeze()
filled_entries = trade.select_filled_orders(trade.entry_side)
last_fill = (current_time - trade.date_last_filled_utc).seconds / 60
count_of_entries = trade.nr_of_successful_entries
count_of_exits = trade.nr_of_successful_exits
buy_sig = last_candle['enter_long']
sell_sig = last_candle['exit_long']
if sell_sig ==1:
if current_profit > 0.003 and count_of_entries == 1:
# Take half of the profit at +5%
return -(trade.stake_amount / count_of_entries)
if current_profit > 0.003 and count_of_entries == 2 and count_of_exits == 0:
# Take half of the profit at +5%
return -(trade.stake_amount / count_of_entries)
if current_profit > 0.003 and count_of_entries == 2 and count_of_exits == 1:
# Take half of the profit at +5%
return -(trade.stake_amount)
if current_profit > 0.003 and count_of_entries == 3 and count_of_exits == 0:
# Take half of the profit at +5%
return -(trade.stake_amount / count_of_entries)
if current_profit > 0.003 and count_of_entries == 3 and count_of_exits == 1:
# Take half of the profit at +5%
return -(trade.stake_amount)
if buy_sig == 1 and (last_fill > self.filldelay.value):
try:
# This returns first order stake size
stake_amount = filled_entries[0].stake_amount
return stake_amount, "Grid Order"
except Exception as exception:
return None
return None
# def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float,
# rate: float, time_in_force: str, exit_reason: str,
# current_time: datetime, **kwargs) -> bool:
# dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
# last_fill = (current_time - trade.date_last_filled_utc).seconds / 60
# count_of_entries = trade.nr_of_successful_entries
# count_of_exits = trade.nr_of_successful_exits
# buy_sig = last_candle['enter_long']
# sell_sig = last_candle['exit_long']
# if exit_reason == 'roi' and trade.calc_profit_ratio(rate) < 0.003:
# logger.info(f"{trade.pair} ROI is below 0%")
# # self.dp.send_msg(f'{trade.pair} ROI is below 0')
# return False
# if exit_reason == 'partial_exit' and trade.calc_profit_ratio(rate) < 0.005:
# logger.info(f"{trade.pair} partial exit is below 0%")
# # self.dp.send_msg(f'{trade.pair} partial exit is below 0')
# return False
# if exit_reason == 'exit_long' and trade.calc_profit_ratio(rate) < 0.01:
# logger.info(f"{trade.pair} preparing trade adjustment")
# # self.dp.send_msg(f'{trade.pair} partial exit is below 0')
# return False
# return True
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# Apply rolling window operation to the 'OHLC4' column
rolling_window = dataframe['close'].rolling(self.roll.value)
# Calculate the peak-to-peak value on the resulting rolling window data
ptp_value = rolling_window.apply(lambda x: np.ptp(x))
# Assign the calculated peak-to-peak value to the DataFrame column
dataframe['cycle_move'] = ptp_value / dataframe['close']
dataframe['cycle_move_mean'] = dataframe['cycle_move'].rolling(self.roll.value).mean()
dataframe["atr"] = ta.ATR(dataframe, timeperiod=12)
dataframe["atrpercent"] = (dataframe["atr"] / dataframe['close']) * 100
box_size_percentage = self.box_size.value
# box_size_percentage = dataframe['cycle_move_mean'].iloc[-1] * 100
reversal_size = box_size_percentage * self.reversal.value
calculate_pnf_with_percentage(dataframe, box_size_percentage, reversal_size)
return dataframe
def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
df.loc[
(
(df["PnF_Column"] == 1) &
(df["PnF_Column"].shift() != 1) &
(df['volume'] > 0) # Make sure Volume is not 0
),
['enter_long', 'enter_tag']] = (1, 'downtrend ends')
return df
def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
df.loc[
(
(df["PnF_Column"] == -1) &
(df["PnF_Column"].shift() != -1) &
(df['volume'] > 0) # Make sure Volume is not 0
),
['exit_long', 'exit_tag']] = (1, 'uptrend ends')
return df
def calculate_pnf_with_percentage(dataframe, box_size_percentage, reversal_size):
# Initialize columns
dataframe["PnF_Column"] = ""
dataframe["PnF_Direction"] = ""
dataframe["PnF_Value"] = None
current_column = None
current_direction = None
current_value = None
for index, row in dataframe.iterrows():
close = row["close"]
box_size = close * (box_size_percentage / 100) # Calculate box size as a percentage
# Default values for the current row
dataframe.loc[index, "PnF_Column"] = current_column
dataframe.loc[index, "PnF_Direction"] = current_direction
dataframe.loc[index, "PnF_Value"] = current_value
# First row initialization
if current_value is None:
current_value = close
current_column = 1 # Assume first column is an uptrend (X)
current_direction = "Up"
dataframe.loc[index, "PnF_Column"] = current_column
dataframe.loc[index, "PnF_Direction"] = current_direction
dataframe.loc[index, "PnF_Value"] = current_value
continue
# Determine the price change
price_change = close - current_value
if current_direction == "Up":
# Continue uptrend or reversal
if price_change >= box_size:
current_value += box_size
dataframe.loc[index, "PnF_Column"] = 1
dataframe.loc[index, "PnF_Direction"] = "Up"
dataframe.loc[index, "PnF_Value"] = current_value
elif price_change <= -reversal_size * box_size:
current_value -= box_size
current_column = -1
current_direction = "Down"
dataframe.loc[index, "PnF_Column"] = current_column
dataframe.loc[index, "PnF_Direction"] = current_direction
dataframe.loc[index, "PnF_Value"] = current_value
elif current_direction == "Down":
# Continue downtrend or reversal
if price_change <= -box_size:
current_value -= box_size
dataframe.loc[index, "PnF_Column"] = -1
dataframe.loc[index, "PnF_Direction"] = "Down"
dataframe.loc[index, "PnF_Value"] = current_value
elif price_change >= reversal_size * box_size:
current_value += box_size
current_column = 1
current_direction = "Up"
dataframe.loc[index, "PnF_Column"] = current_column
dataframe.loc[index, "PnF_Direction"] = current_direction
dataframe.loc[index, "PnF_Value"] = current_value
return dataframe