The strategy trades Bitcoin based on overreaction days, buying on positive overreactions and selling on negative ones, with positions opened at 18:00 or 16:00 and closed at 00:00.

I. STRATEGY IN A NUTSHELL

The strategy trades Bitcoin by buying on positive overreaction days and selling on negative ones. An overreaction day occurs when returns exceed the average plus two standard deviations. Positions are opened at 18:00 (positive) or 16:00 (negative) and closed at 00:00, capturing significant intraday price movements.

II. ECONOMIC RATIONALE

Overreaction-driven momentum persists intraday, driven by behavioral biases such as herding, overreaction, and confirmation bias. These effects are amplified in markets with high retail participation, making Bitcoin particularly responsive to momentum-based strategies.

III. SOURCE PAPER

Momentum Effects in the Cryptocurrency Market after One-Day Abnormal Returns [Click to Open PDF]

Caporale, Guglielmo Maria; Plastun, Alex — Brunel University London – Department of Economics and Finance; London South Bank University; CESifo (Center for Economic Studies and Ifo Institute); German Institute for Economic Research (DIW Berlin); Sumy State University.

<Abstract>

This paper examines whether there exists a momentum effect after one-day abnormal returns in the cryptocurrency market. For this purpose a number of hypotheses of interest are tested for the BitCoin, Ethereum and LiteCoin exchange rates vis-à-vis the US dollar over the period 01.01.2017-01.09.2019, specifically whether or not: H1) the intraday behaviour of hourly returns is different on overreaction days compared to normal days; H2) there is a momentum effect on overreaction days, and H3) after one-day abnormal returns. The methods used for the analysis include a number of statistical methods as well as a trading simulation approach. The results suggest that hourly returns during the day of positive/negative overreactions are significantly higher/lower than those during the average positive/negative day. Overreactions can usually be detected before the day ends by estimating specific timing parameters. Prices tend to move in the direction of the overreaction till the end of the day when it occurs, which implies the existence of a momentum effect on that day giving rise to exploitable profit opportunities. This effect (together with profit opportunities) is also observed on the following day. In two cases (BTCUSD positive overreactions and ETHUSD negative overreactions) a contrarian effect is detected instead.

IV. BACKTEST PERFORMANCE

Annualised Return28.62%
Volatility30.26%
Beta-0.052
Sharpe Ratio0.95
Sortino Ratio0.051
Maximum DrawdownN/A
Win Rate52%

V. FULL PYTHON CODE

from AlgorithmImports import *
from pandas.core.frame import dataframe
from math import floor
#endregion
class BitcoinIntradayMomentum(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetCash('USD', 100000)
        self.closes:List[float] = []
        self.period:int = 12 * 21
        self.SetWarmup(self.period, Resolution.Daily)
        
        self.percentage_traded:float = .9
        self.std_threshold:float = 2.
        self.signal_hours:List[int] = [16, 18]
        
        self.symbol:Symbol = self.AddCrypto('BTCUSD', Resolution.Minute, Market.Bitfinex).Symbol
    def OnData(self, data: Slice) -> None:
        if not (self.symbol in data and data[self.symbol]):
            return
        
        current_price:float = data[self.symbol].Value
        if self.Time.hour == 23 and self.Time.minute == 59:
            # store daily price
            self.closes.append(current_price)
            if self.Portfolio[self.symbol].Invested:
                self.Liquidate(self.symbol)
        if self.IsWarmingUp: return
        if len(self.closes) < self.period: return
        if (self.Time.hour in self.signal_hours and self.Time.minute == 0):
        
            # daily return return calculation
            last_close:float = self.closes[-1]
            performance:float = current_price / last_close - 1
            
            # daily return average
            closes:np.ndarray = np.array(self.closes)
            daily_returns:np.ndarray = closes[1:] / closes[:-1] - 1
            ret_mean:float = np.mean(daily_returns)
            ret_std:float = np.std(daily_returns)
            q:float = floor(self.Portfolio.TotalPortfolioValue * self.percentage_traded / current_price)
            if q >= self.Securities[self.symbol].SymbolProperties.MinimumOrderSize:
                # overreaction handling
                if self.Time.hour == self.signal_hours[0] and self.Time.minute == 0:
                    if not self.Portfolio[self.symbol].Invested:
                        if performance < ret_mean - self.std_threshold * ret_std:
                            self.MarketOrder(self.symbol, -q)
                
                elif self.Time.hour == self.signal_hours[1] and self.Time.minute == 0:
                    if not self.Portfolio[self.symbol].Invested:
                        if performance > ret_mean + self.std_threshold * ret_std:
                            self.MarketOrder(self.symbol, q)

VI. Backtest Performance

Leave a Reply

Discover more from Quant Buffet

Subscribe now to keep reading and get access to the full archive.

Continue reading