ST – Holding Strategy

This is one of the fried chicken strategies that I’m using currently. The idea is simple, if I believe that a coin’s price will always go up in long term, then why not just hold it instead of doing many trades of it?

As said in the linked post above, the most important part is the pair(s) to be used. Personally, I only trade BTC using this strategy, but you are free to choose your own pair(s) as long as you believe that those pairs suits the criteria.

Please read the README part at the end of the code below

import logging
from datetime import datetime
from freqtrade.strategy import (
    IStrategy,
    Order,
    Trade,
)
from pandas import DataFrame
from dataclasses import dataclass

logger = logging.getLogger(__name__)


@dataclass
class TradeData:
    roi: float = 0
    grid_count: int = 0
    grid_amount: float = 0
    base_stake: float = 0
    price_for_grid: float = 0


trade_data_dict: dict[str, TradeData] = {}
grid_entry_tag = "grid entry"
grid_exit_tag = "grid exit"
positive_dca_entry_tag = "positive dca entry"
roi = 1
grid_distance = 0.015
grid_roi = 0.015
positive_dca_level = 0.05


class hold(IStrategy):
    INTERFACE_VERSION = 3

    timeframe = "15m"

    minimal_roi = {
        "0": roi,
    }

    # Stoploss:
    stoploss = -1

    process_only_new_candles = True
    startup_candle_count = 1

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        return dataframe

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe["enter_long"] = 1

        return dataframe

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe["exit_long"] = 0

        return dataframe

    def custom_stake_amount(
        self,
        pair: str,
        current_time: datetime,
        current_rate: float,
        proposed_stake: float,
        min_stake: float | None,
        max_stake: float,
        leverage: float,
        entry_tag: str | None,
        side: str,
        **kwargs,
    ) -> float:
        return max(min_stake, proposed_stake)

    position_adjustment_enable = True
    max_entry_position_adjustment = -1

    def adjust_trade_position(
        self,
        trade: Trade,
        current_time: datetime,
        current_rate: float,
        current_profit: float,
        min_stake: float | None,
        max_stake: float,
        current_entry_rate: float,
        current_exit_rate: float,
        current_entry_profit: float,
        current_exit_profit: float,
        **kwargs,
    ) -> float | None | tuple[float | None, str | None]:
        try:
            has_open_entry = False
            has_open_exit = False

            open_orders = trade.open_orders

            if len(open_orders) > 0:
                for order in open_orders:
                    if order.ft_order_side == trade.entry_side:
                        has_open_entry = True
                    else:
                        has_open_exit = True

            filled_entries = trade.select_filled_orders(trade.entry_side)
            count_of_entries = len(filled_entries)

            if count_of_entries > 0:
                fill_trade_data(trade, trade_data_dict)
                trade_data = trade_data_dict[trade.id]
                grid_count = trade_data.grid_count

                if (grid_count > 0) and not has_open_exit:
                    if current_profit >= grid_roi:
                        grid_amount = trade_data.grid_amount
                        return (
                            -grid_amount * trade.stake_amount / trade.amount,
                            grid_exit_tag,
                        )

                if current_profit >= positive_dca_level:
                    new_stake = max(min_stake or 0, trade_data.base_stake)

                    if new_stake > 0:
                        return new_stake, positive_dca_entry_tag

            if has_open_entry:
                return None

            dataframe, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe)
            last_candle = dataframe.iloc[-1]
            last_candle_close = last_candle["close"]

            if trade.open_rate <= current_rate:
                return None

            if last_candle["open"] <= last_candle_close:
                return None

        except Exception:
            return None

        try:
            grid_count_check = grid_count + 1
            grid_level_check = -1 * grid_count_check * grid_distance
            price_for_grid = trade_data.price_for_grid

            if price_for_grid > 0:
                current_profit_first_entry = trade.calc_profit_ratio(
                    rate=current_rate, open_rate=price_for_grid, amount=trade.amount
                )
                if current_profit_first_entry <= grid_level_check:
                    new_stake = max(min_stake or 0, trade_data.base_stake)

                    if new_stake > 0:
                        return new_stake, f"{grid_entry_tag}{grid_count_check}"

        except Exception:
            return None

        return None

    def order_filled(
        self, pair: str, trade: Trade, order: Order, current_time: datetime, **kwargs
    ) -> None:
        if order.ft_order_side == trade.entry_side:
            if trade.nr_of_successful_entries == 1:
                base_stake = order.stake_amount_filled
                price_grid = order.safe_price

                trade_data_dict[trade.id] = TradeData()
                trade_dict = trade_data_dict[trade.id]
                trade_dict.roi = roi
                trade_dict.base_stake = base_stake
                trade_dict.price_for_grid = price_grid

                trade.set_custom_data(key="base_stake", value=base_stake)
                trade.set_custom_data(key="roi", value=roi)
                trade.set_custom_data(key="price_for_grid", value=price_grid)

            else:
                fill_trade_data(trade, trade_data_dict)

                order_tag = order.ft_order_tag.strip()

                if order_tag.startswith(grid_entry_tag):
                    set_grid_data(
                        trade=trade, order=order, trade_data_dict=trade_data_dict
                    )
                elif order_tag.startswith(positive_dca_entry_tag):
                    update_price_for_grid(trade=trade, trade_data_dict=trade_data_dict)

        else:
            order_tag = order.ft_order_tag or ""
            if order_tag.strip().startswith(grid_exit_tag):
                set_grid_data(
                    trade=trade,
                    order=order,
                    trade_data_dict=trade_data_dict,
                )

            if order.safe_filled == trade.amount:
                trade_data_dict.pop(trade.id, None)


def fill_trade_data(trade: Trade, trade_data_dict: dict[str, TradeData]) -> None:
    filled_entries = trade.select_filled_orders(trade.entry_side)
    count_of_entries = len(filled_entries)
    if (trade.id not in trade_data_dict) and (count_of_entries > 0):
        logger.info(f"Filling trade data {trade.id} to trade_data_dict.")

        first_entry_order = trade.select_filled_orders(trade.entry_side)[0]

        try:
            roi = trade.get_custom_data(key="roi", default=0.005)
        except Exception as exception:
            logger.error(f"Error getting ROI for trade {trade.id}: {str(exception)}")
            roi = 0.005

        try:
            base_stake = trade.get_custom_data(
                key="base_stake", default=first_entry_order.stake_amount_filled
            )
        except Exception as exception:
            logger.error(
                f"Error getting base stake for trade {trade.id}: {str(exception)}"
            )
            base_stake = first_entry_order.stake_amount_filled

        try:
            grid_count = trade.get_custom_data(key="grid_count", default=0)
        except Exception as exception:
            logger.error(
                f"Error getting grid count for trade {trade.id}: {str(exception)}"
            )
            grid_count = 0

        try:
            grid_amount = trade.get_custom_data(key="grid_amount", default=0)
        except Exception as exception:
            logger.error(
                f"Error getting grid amount for trade {trade.id}: {str(exception)}"
            )
            grid_amount = 0

        try:
            price_for_grid = trade.get_custom_data(
                key="price_for_grid", default=first_entry_order.safe_price
            )
        except Exception as exception:
            logger.error(
                f"Error getting price for grid for trade {trade.id}: {str(exception)}"
            )
            price_for_grid = first_entry_order.safe_price

        trade_data_dict[trade.id] = TradeData()

        trade_dict = trade_data_dict[trade.id]
        trade_dict.roi = roi
        trade_dict.base_stake = base_stake
        trade_dict.grid_count = int(grid_count)
        trade_dict.grid_amount = grid_amount
        trade_dict.price_for_grid = price_for_grid
        logger.info(
            f"Filling trade #{trade.id}'s custom data to trade_data_dict, values are {trade_dict}"
        )


def set_grid_data(
    trade: Trade,
    order: Order,
    trade_data_dict: dict[str, TradeData],
) -> None:
    trade_data = trade_data_dict[trade.id]
    old_grid_count = trade_data.grid_count
    old_grid_amount = trade_data.grid_amount
    tag = order.ft_order_tag
    update_data = False
    if order.ft_order_side == trade.entry_side:
        new_grid_count = int(tag.replace(grid_entry_tag, "").strip())
        new_grid_amount = old_grid_amount + order.safe_filled
        update_data = old_grid_count != new_grid_count
    else:
        new_grid_amount = order.safe_remaining
        new_grid_count = 0 if new_grid_amount == 0 else old_grid_count
        update_data = True

    if update_data:
        trade.set_custom_data(key="grid_count", value=new_grid_count)
        trade.set_custom_data(key="grid_amount", value=new_grid_amount)
        trade_data.grid_count = new_grid_count
        trade_data.grid_amount = new_grid_amount

        if new_grid_amount == 0:
            update_price_for_grid(trade=trade, trade_data_dict=trade_data_dict)


def update_price_for_grid(trade: Trade, trade_data_dict: dict[str, TradeData]) -> None:
    trade_data = trade_data_dict[trade.id]
    new_price = trade.open_rate
    trade.set_custom_data(key="price_for_grid", value=new_price)
    trade_data.price_for_grid = new_price


# README
# This strategy is a simple hold strategy with DCA features.
# There is no entry or exit logic, it will always enters.
# Full exit is triggered by ROI only.
# Partial exit is triggered when grid_roi is reached.
# Additional entries are made on 2 conditions:
# 1. Positive DCA: when the trade's overall open profit is greater than positive_dca_level, it will add an additional entry with the same stake as the base stake.
# 2. Negative DCA: the profit % used for this is based of price_for_grid value, which is initially set to the first entry price, and updated on every partial exit or positive DCA entry.
#
# roi is intentionally being set to high value to avoid full exit.
# Only use on pairs that you are sure will go up over time.
# MUST USE static stake amount.
#
# You can adjust the grid_distance, grid_roi and positive_dca_level to your liking.

One comment

Leave a Reply