Skip to content
All library documents

OTT and Variable Moving Average Crossovers in a Freqtrade Strategy

Code Freqtrade

Summary

This Freqtrade strategy uses a variable moving average and an OTT trailing-line calculation to generate directional entries on a one-hour chart. It enters long when the variable average crosses above OTT and short when it crosses below. The OTT function derives a variable average using a Chande-style momentum ratio, builds percentage-offset long and short stops, tracks direction changes, and shifts the resulting OTT line by two candles. ADX is also calculated, and readings above 60 trigger exits for either side.

The code sets a tiered return-on-investment table, a fixed stop loss, and a trailing stop with specified thresholds. Its comments say some parameters came from hyperparameter optimization, while encouraging users to find settings suited to their needs and risk controls. The document includes implementation details but no backtest results, market universe, or evidence that the signals are profitable. The parameter choices and indicator behavior therefore require independent evaluation, including attention to timing, fees, and short-position availability on the intended exchange.

Key ideas

  • Long and short entries are triggered by opposite crossovers between a variable average and the OTT line.
  • The OTT calculation uses a momentum-weighted average and percentage-offset trailing stops to track direction.
  • ADX above 60 is used as an exit condition for both long and short positions.
  • The strategy also defines ROI targets, a stop loss, and trailing-stop settings.
  • The code provides no backtest evidence, so its parameters and signal behavior need independent evaluation.

Tags

Full text
# FOttStrategy.py


```py
import logging
from numpy.lib import math
from freqtrade.strategy import IStrategy
from pandas import DataFrame
import talib.abstract as ta
import numpy as np
import freqtrade.vendor.qtpylib.indicators as qtpylib



class FOttStrategy(IStrategy):
    # Buy params, Sell params, ROI, Stoploss and Trailing Stop are values generated by 'freqtrade hyperopt --strategy Supertrend --hyperopt-loss ShortTradeDurHyperOptLoss --timerange=20210101- --timeframe=1h --spaces all'
    # It's encourage you find the values that better suites your needs and risk management strategies

    INTERFACE_VERSION: int = 3
    # ROI table:
    minimal_roi = {"0": 0.1, "30": 0.75, "60": 0.05, "120": 0.025}
    # minimal_roi = {"0": 1}

    # Stoploss:
    stoploss = -0.265

    # Trailing stop:
    trailing_stop = True
    trailing_stop_positive = 0.05
    trailing_stop_positive_offset = 0.1
    trailing_only_offset_is_reached = False

    timeframe = "1h"

    startup_candle_count = 18

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

        dataframe["ott"] = self.ott(dataframe)["OTT"]
        dataframe["var"] = self.ott(dataframe)["VAR"]
        dataframe["adx"] = ta.ADX(dataframe, timeperiod=14)

        return dataframe

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

        dataframe.loc[
            (qtpylib.crossed_above(dataframe["var"], dataframe["ott"])),
            "enter_long",
        ] = 1

        dataframe.loc[
            (qtpylib.crossed_below(dataframe["var"], dataframe["ott"])),
            "enter_short",
        ] = 1

        return dataframe

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe.loc[
            (
                dataframe["adx"]>60
            ),
            "exit_long",
        ] = 1

        dataframe.loc[
            (
                dataframe["adx"]>60
            ),
            "exit_short",
        ] = 1

        return dataframe

    """
        Supertrend Indicator; adapted for freqtrade
        from: https://github.com/freqtrade/freqtrade-strategies/issues/30
    """

    def ott(self, dataframe: DataFrame):
        df = dataframe.copy()

        pds = 2
        percent = 1.4
        alpha = 2 / (pds + 1)

        df["ud1"] = np.where(
            df["close"] > df["close"].shift(1), (df["close"] - df["close"].shift()), 0
        )
        df["dd1"] = np.where(
            df["close"] < df["close"].shift(1), (df["close"].shift() - df["close"]), 0
        )
        df["UD"] = df["ud1"].rolling(9).sum()
        df["DD"] = df["dd1"].rolling(9).sum()
        df["CMO"] = ((df["UD"] - df["DD"]) / (df["UD"] + df["DD"])).fillna(0).abs()

        # df['Var'] = talib.EMA(df['close'], timeperiod=5)
        df["Var"] = 0.0
        for i in range(pds, len(df)):
            df["Var"].iat[i] = (alpha * df["CMO"].iat[i] * df["close"].iat[i]) + (
                1 - alpha * df["CMO"].iat[i]
            ) * df["Var"].iat[i - 1]

        df["fark"] = df["Var"] * percent * 0.01
        df["newlongstop"] = df["Var"] - df["fark"]
        df["newshortstop"] = df["Var"] + df["fark"]
        df["longstop"] = 0.0
        df["shortstop"] = 999999999999999999
        # df['dir'] = 1
        for i in df["UD"]:

            def maxlongstop():
                df.loc[(df["newlongstop"] > df["longstop"].shift(1)), "longstop"] = df[
                    "newlongstop"
                ]
                df.loc[(df["longstop"].shift(1) > df["newlongstop"]), "longstop"] = df[
                    "longstop"
                ].shift(1)

                return df["longstop"]

            def minshortstop():
                df.loc[
                    (df["newshortstop"] < df["shortstop"].shift(1)), "shortstop"
                ] = df["newshortstop"]
                df.loc[
                    (df["shortstop"].shift(1) < df["newshortstop"]), "shortstop"
                ] = df["shortstop"].shift(1)

                return df["shortstop"]

            df["longstop"] = np.where(
                ((df["Var"] > df["longstop"].shift(1))),
                maxlongstop(),
                df["newlongstop"],
            )

            df["shortstop"] = np.where(
                ((df["Var"] < df["shortstop"].shift(1))),
                minshortstop(),
                df["newshortstop"],
            )

        # get xover

        df["xlongstop"] = np.where(
            (
                (df["Var"].shift(1) > df["longstop"].shift(1))
                & (df["Var"] < df["longstop"].shift(1))
            ),
            1,
            0,
        )

        df["xshortstop"] = np.where(
            (
                (df["Var"].shift(1) < df["shortstop"].shift(1))
                & (df["Var"] > df["shortstop"].shift(1))
            ),
            1,
            0,
        )

        df["trend"] = 0
        df["dir"] = 0
        for i in df["UD"]:
            df["trend"] = np.where(
                ((df["xshortstop"] == 1)),
                1,
                (np.where((df["xlongstop"] == 1), -1, df["trend"].shift(1))),
            )

            df["dir"] = np.where(
                ((df["xshortstop"] == 1)),
                1,
                (np.where((df["xlongstop"] == 1), -1, df["dir"].shift(1).fillna(1))),
            )

        # get OTT

        df["MT"] = np.where(df["dir"] == 1, df["longstop"], df["shortstop"])
        df["OTT"] = np.where(
            df["Var"] > df["MT"],
            (df["MT"] * (200 + percent) / 200),
            (df["MT"] * (200 - percent) / 200),
        )
        df["OTT"] = df["OTT"].shift(2)

        return DataFrame(index=df.index, data={"OTT": df["OTT"], "VAR": df["Var"]})


```

Shown in full with attribution under the source's licence. Licence: GPL-3.0

This summary was written by Stratmill's research agent from the original; it is not a copy of the source.