Bitcoin Intraday Momentum
Log in to collectAcademic paper
Momentum Effects in the Cryptocurrency Market after One-Day Abnormal Returns
Guglielmo Maria Caporale; Alex Plastun
- 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)