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.
[…] Holding Strategy […]