How to Implement Overnight Seasonality in Bitcoin: A Systematic Trading Strategy
Overnight seasonality in Bitcoin exploits the systematic return between the daily close and the next open by entering long positions when the previous overnight return was positive and short positions when it was negative, holding the exposure throughout the next trading day.
The overnight seasonality effect in Bitcoin captures predictable price drift during the after-hours window, distinct from intraday momentum patterns. This guide walks through a complete implementation using the paperswithbacktest/awesome-systematic-trading repository, leveraging existing data-handling patterns and back-testing infrastructure to deploy a production-ready strategy.
Understanding the Overnight Seasonality Signal
The strategy rests on the empirical observation that Bitcoin exhibits persistent directional bias during the overnight window—the period between the close of day t-1 and the open of day t. The core signal derives from the lagged overnight return, calculated as:
[ r^{\text{overnight}}t = \frac{\text{Open}t - \text{Close}{t-1}}{\text{Close}{t-1}} ]
When this return is positive, it suggests buying pressure accumulated during the after-hours session, predicting continued strength during the following trading day. Conversely, negative overnight returns indicate selling pressure. The trading rule enters positions at the open and exits at the close, capturing the full intraday session while avoiding overnight risk.
Step-by-Step Implementation
Load Daily OHLCV Data
The strategy requires daily-resolution data containing Open, High, Low, Close, and Volume fields. The repository demonstrates standard data ingestion patterns in static/strategies/intraday-seasonality-in-bitcoin.py, which handles timestamp alignment and missing data interpolation.
import yfinance as yf
import pandas as pd
# Download daily OHLCV for Bitcoin
btc = yf.download("BTC-USD", start="2015-01-01", interval="1d")
Compute Overnight Returns
Calculate the close-to-open return for each trading day. This represents the price gap between the previous day's settlement and the current session's opening print.
# Compute overnight returns: (Open_t - Close_{t-1}) / Close_{t-1}
btc["overnight_ret"] = (btc["Open"] - btc["Close"].shift(1)) / btc["Close"].shift(1)
Generate Trading Signals
Form a binary signal based on the sign of the lagged overnight return. The position is held from the current open to the current close, capturing the intraday drift predicted by the overnight gap.
import numpy as np
# Binary signal: +1 for long (previous overnight return > 0), -1 for short (< 0)
btc["signal"] = np.sign(btc["overnight_ret"].shift(1))
# Intraday return (Open to Close)
btc["intraday_ret"] = (btc["Close"] - btc["Open"]) / btc["Open"]
# Strategy returns: signal applied to next day's intraday session
btc["strategy_ret"] = btc["signal"].shift(1) * btc["intraday_ret"]
Complete Code Implementation
Below is a standalone implementation incorporating signal smoothing and volatility scaling, compatible with the repository's requirements.txt dependencies.
import yfinance as yf
import pandas as pd
import numpy as np
def overnight_seasonality_strategy(ticker="BTC-USD", smoothing=5, vol_target=0.01):
# 1. Load data
data = yf.download(ticker, start="2015-01-01", interval="1d")
# 2. Calculate overnight returns
data["overnight_ret"] = data["Open"].pct_change()
# 3. Optional: Smooth signal with rolling mean to reduce noise
data["signal_raw"] = data["overnight_ret"].rolling(window=smoothing).mean()
data["signal"] = np.sign(data["signal_raw"].shift(1))
# 4. Calculate intraday returns for execution
data["intraday_ret"] = (data["Close"] - data["Open"]) / data["Open"]
# 5. Volatility scaling (optional risk management)
data["vol"] = data["intraday_ret"].rolling(20).std()
data["scaled_position"] = data["signal"] * (vol_target / data["vol"].shift(1))
# 6. Strategy performance
data["strategy_ret"] = data["scaled_position"].shift(1) * data["intraday_ret"]
data["cum_returns"] = (1 + data["strategy_ret"].fillna(0)).cumprod()
return data
# Run strategy
results = overnight_seasonality_strategy()
print(f"Annualized Return: {results['strategy_ret'].mean() * 252:.2%}")
print(f"Sharpe Ratio: {results['strategy_ret'].mean() / results['strategy_ret'].std() * np.sqrt(252):.2f}")
Integration with the Repository Framework
The paperswithbacktest/awesome-systematic-trading repository provides a generic back-testing harness in src/backtest.py that standardizes strategy implementation. To integrate the overnight seasonality strategy, create a new class following the interface demonstrated in static/strategies/intraday-seasonality-in-bitcoin.py.
Required methods for the framework:
init(self, data): Accepts the raw OHLCV DataFrame and pre-computes overnight returns and signal vectors.generate_signals(self): Returns a pandas Series of daily positions (+1, -1, or 0) aligned with the data index.
Reference the complementary implementation in static/strategies/market-sentiment-and-an-overnight-anomaly.py for equity markets, which demonstrates the exact structure for computing overnight returns and applying quantile-based filters adaptable to Bitcoin.
Key Implementation Considerations
Data Frequency: Daily OHLCV is sufficient. Minute-level data introduces unnecessary noise since the overnight window is explicitly defined by the close-to-open gap.
Missing Data Handling: While Bitcoin trades 24/7, data providers may omit specific timestamps. Use forward-fill for missing closes or drop incomplete rows to maintain correct lagged alignment between consecutive days.
Transaction Costs: The strategy executes once per day (entry at open, exit at close), minimizing slippage. Assume realistic commission rates (e.g., 0.02% per trade) in back-tests to avoid overestimation of net returns.
Risk Management: Apply a maximum drawdown circuit breaker or volatility targeting (capping daily volatility at 1%) to mitigate exposure during extreme market regimes. The volatility-scaling example above demonstrates dynamic position sizing based on a 20-day rolling standard deviation.
Summary
- Overnight seasonality in Bitcoin exploits the close-to-open return as a predictive signal for the following intraday session.
- The implementation requires daily OHLCV data and calculates the lagged overnight return to determine long/short positions.
- Reference implementations exist in
static/strategies/intraday-seasonality-in-bitcoin.pyandstatic/strategies/market-sentiment-and-an-overnight-anomaly.py. - The
src/backtest.pyframework requiresinit()andgenerate_signals()methods for integration. - Signal smoothing and volatility scaling enhance robustness against Bitcoin's high-volatility regimes.
Frequently Asked Questions
What is the overnight seasonality effect in Bitcoin?
The overnight seasonality effect refers to the tendency of Bitcoin's price to drift systematically during the period between the daily close and the next day's open. This anomaly suggests that information accumulated during after-hours trading creates predictable momentum that persists into the next regular trading session, allowing traders to capture returns by positioning at the open based on the previous night's gap direction.
How do I calculate the overnight return for Bitcoin?
Calculate the overnight return as the percentage change from the previous day's close to the current day's open using the formula: (Open_t - Close_{t-1}) / Close_{t-1}. In Python with pandas, this translates to df["Open"].pct_change() when working with daily data, or explicitly (df["Open"] - df["Close"].shift(1)) / df["Close"].shift(1) to ensure precise alignment of timestamps.
Can I use this strategy with other cryptocurrencies?
Yes, the overnight seasonality logic applies to any asset with reliable daily OHLCV data, including Ethereum and other major cryptocurrencies. However, the strength of the signal varies by asset; Bitcoin typically exhibits the most pronounced overnight effect due to its higher institutional after-hours trading volume. Always validate the signal on out-of-sample data for each specific ticker before deployment.
How does this differ from intraday seasonality strategies?
While intraday seasonality focuses on patterns within a single trading day (such as time-of-day effects or momentum between specific hours), overnight seasonality specifically isolates the price gap between sessions. The overnight strategy holds positions only during regular trading hours (open-to-close), whereas intraday strategies may involve multiple round trips or specific entry times within the day. The repository's intraday-seasonality-in-bitcoin.py file demonstrates these internal-day patterns, contrasting with the close-to-open focus of overnight implementations.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →