
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.
ASSET CLASS: cryptos | REGION: Global | FREQUENCY:
Intraday | MARKET: cryptos | KEYWORD: Bitcoin, Momentum
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 Return | 28.62% |
| Volatility | 30.26% |
| Beta | -0.052 |
| Sharpe Ratio | 0.95 |
| Sortino Ratio | 0.051 |
| Maximum Drawdown | N/A |
| Win Rate | 52% |
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