Quant BuffetRelax, Not Over Thinking

Bitcoin Intraday Momentum

Log in to collect

Academic paper

Momentum Effects in the Cryptocurrency Market after One-Day Abnormal Returns

AuthorsGuglielmo Maria Caporale; Alex Plastun

Institute
  • Brunel University of London
  • London South Bank University
  • DEGerman Institute for Economic Research
  • DEIfo Institute for Economic Research
  • ?Brunel University London - Department of Economics and Finance
  • ?CESifo (Center for Economic Studies and Ifo Institute)
  • ?German Institute for Economic Research (DIW Berlin)
  • UASumy State University

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.

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.

Backtest performance

Annualised return28.62%
Volatility30.26%
Beta-0.052
Sharpe ratio0.95
Sortino ratio0.051
Win rate52%

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)