Beyond Buy and Hold: A Scientific Approach to Catching Bitcoin’s Big Moves
Objective
Develop a trading strategy based on BTC long-term trend reversals. The strategy executes bi-directional trades on daily timeframes, aiming to achieve stable long-term profits by capturing significant trend reversal points.
Development Platform: Zorro
Development Method: Adopting an iterative development approach, starting with basic trend indicators and gradually adding and validating new trading mechanisms. Each iteration tests only 1–2 key parameters, and mechanisms are retained if parameters show stable performance in the training set and significantly improve strategy performance in out-of-sample testing.
Backtesting Setup
- Trading Instrument: BTCUSDT Perpetual Futures (Binance)
- Timeframe: Daily
- Sample Period: 2014.01.01–2024.12.30
- Spread: 0.1
- Commission: 0.1% round-trip (0.05% entry, 0.05% exit)
- Slippage: 5-second delay simulation
- Walk-Forward Optimization (WFO) Settings:
- Using anchored WFA method for out-of-sample validation
- Training set: 80%, Testing set: 20%
- Retraining approximately annually, total 10 rounds
- Double weighting for trades in the latter part of training set to enhance parameter adaptability to market changes
Source codes
/*
Bitcoin Trend Reversal Strategy v2
*/
var fisherTransform(vars Sources, int Period) {
// Calculate the highest and lowest values of the source series
var Highest = MaxVal(Sources, Period);
var Lowest = MinVal(Sources, Period);
// Calculate the value of the Fisher Transform
vars Values = series(0., 2);
vars Fishers = series(0., 2);
Values[0] = 0.66*((Sources[0]-Lowest)/(Highest-Lowest)-0.5) + 0.67*Values[1];
Values[0] = max(min(Values[0], 0.999), -0.999);
Fishers[0] = 0.5*log((1+Values[0])/(1-Values[0])) + 0.5*Fishers[1];
// Return the Fisher Transform of the source series
return Fishers[0];
}
function adaptiveExitLong(vars Fishers, var ProfitThreshold) {
int ExitOrders = 0;
if(crossOver(Fishers, 2.)) ExitOrders = 1;
if(crossOver(Fishers, 3.)) ExitOrders = 2;
if(crossOver(Fishers, 4.)) ExitOrders = 3;
if(ExitOrders > 0) {
int ExitCounter = 0;
for(current_trades) {
if(TradeIsOpen && TradeIsLong && (priceC() - TradePriceOpen) > ProfitThreshold*ATR(100)) {
exitTrade(ThisTrade);
ExitCounter++;
if(ExitCounter >= ExitOrders) return_trades;
}
}
}
}
function adaptiveExitShort(vars Fishers, var ProfitThreshold) {
int ExitOrders = 0;
if(crossUnder(Fishers, -2.)) ExitOrders = 1;
if(crossUnder(Fishers, -3.)) ExitOrders = 2;
if(crossUnder(Fishers, -4.)) ExitOrders = 3;
if(ExitOrders > 0) {
int ExitCounter = 0;
for(current_trades) {
if(TradeIsOpen && TradeIsShort && (TradePriceOpen - priceC()) > ProfitThreshold*ATR(100)) {
exitTrade(ThisTrade);
ExitCounter++;
if(ExitCounter >= ExitOrders) return_trades;
}
}
}
}
function enterLongSplitOrders(int Orders) {
int LotsPerOrder = (int)floor(Lots / Orders);
int i;
for(i=1; i<=Orders; i++) {
enterLong(LotsPerOrder);
}
}
function enterShortSplitOrders(int Orders) {
int LotsPerOrder = (int)floor(Lots / Orders);
int i;
for(i=1; i<=Orders; i++) {
enterShort(LotsPerOrder);
}
}
var fixedRiskLots(var FixedRisk) {
var CapitalGrowthFactor = 1.+(WinTotal-LossTotal)/Capital;
return floor(Capital*sqrt(CapitalGrowthFactor)*FixedRisk/Stop/LotAmount);
}
function run() {
// --------------------------------------------------------- //
// Zorro settings
// --------------------------------------------------------- //
// Set up system flags
set(LOGFILE,PLOTNOW,PARAMETERS);
// Chart settings
setf(PlotMode, PL_FINE|PL_DIFF);
PlotScale = 8;
PlotHeight2 = 320;
// K-line generation, must be set before asset()
resf(BarMode,BR_WEEKEND);
StartWeek = 0;
EndWeek = 62359;
StartMarket = 0;
EndMarket = 2359;
BarPeriod = 1440;
BarZone = UTC;
BarOffset = 0;
TickFix = 60000;
// WFO settings
setf(TrainMode, TRADES); // Training and test sets use the same trading amount
DataSlope = 2; // 2x weight to trades at the end of the training set
DataSplit = 80; // 80% training, 20% test
NumWFOCycles = -10; // 10 cycles of out-of-sample testing, -10 means anchored WFO
// Backtest range
StartDate = 20140101;
EndDate = 20241230;
LookBack = 600;
// Backtest asset
asset("BTCUSDT");
// Asset specific parameters, must be set after asset()
Spread = 0.1; // Bid/ask spreads
RollLong = RollShort = 0; // No rollover costs
PIP = 0.1; // Minimum price unit, 10000.1 -> 10000.2
PIPCost = 0.0001; // Dollar value(USDT) of a one-pip price movement
LotAmount = 0.001; // Lot size in BTC amount, 1 Lot -> 0.001 BTC
Commission = -0.1; // Commission in percentage, -0.1 means 0.1% commission(round trip)
Slippage = 5; // 5-second delay for order execution
Leverage = 1; // No leverage
// Fill orders at open of next bar
Fill = 3;
// Defaul trade amount(1 BTC), will be modified by money management
Lots = 1000;
// Initial capital
Capital = 10000;
// --------------------------------------------------------- //
// Strategy logic
// --------------------------------------------------------- //
// Parameters
int TrendPeriod = round(optimize(200, 100, 300, 10)); // Period of lowpass filter
int CyclePeriod = 10; // Period of fisher transform, if 0 then no reentry
int RegimePeriod = 0; // Period of regime filter, if 0 then no regime filter
var RegimeThreshold = 1.4; // Threshold for regime filter
var ProfitThreshold = optimize(0, 3, 20, 1); // 5 means 5 ATR profit, if 0 then no dynamic exit
int SplitOrders = 10; // Total split orders for dynamic exit
var AtrStopFactor = optimize(0, 1, 4, 0.2); // Atr-based stop loss, if 0 then no ATR stop
var FixedRisk = 0.02; // Fixed risk per trade, if 0 then use fixed lots
// Indicators
vars Prices = series(priceC());
vars Trends = series(LowPass(Prices, TrendPeriod));
vars Cycles = series(ifelse(CyclePeriod, fisherTransform(Prices, CyclePeriod), 0));
var Regime = ifelse(RegimePeriod, FractalDimension(Prices, RegimePeriod), 0);
// Signals
bool LongEntry = valley(Trends);
bool LongExit = peak(Trends);
bool ShortEntry = peak(Trends);
bool ShortExit = valley(Trends);
if(CyclePeriod) {
LongEntry = LongEntry || (rising(Trends) && Cycles[0] < 0 && crossOver(Cycles, Cycles+1));
ShortEntry = ShortEntry || (falling(Trends) && Cycles[0] > 0 && crossUnder(Cycles, Cycles+1));
}
if(RegimePeriod) {
LongEntry = LongEntry && Regime > RegimeThreshold;
ShortEntry = ShortEntry && Regime > RegimeThreshold;
}
// Trading
Stop = AtrStopFactor*ATR(20);
if(!Train && Capital > 0 && FixedRisk > 0 && AtrStopFactor > 0) {
Lots = fixedRiskLots(FixedRisk);
}
if(NumOpenLong > 0 && LongExit)
exitLong();
if(NumOpenShort > 0 && ShortExit)
exitShort();
if(NumOpenLong == 0 && LongEntry)
if(ProfitThreshold > 0 && SplitOrders > 1)
enterLongSplitOrders(SplitOrders);
else
enterLong();
if(NumOpenShort == 0 && ShortEntry)
if(ProfitThreshold > 0 && SplitOrders > 1)
enterShortSplitOrders(SplitOrders);
else
enterShort();
// Adaptive exit
if(ProfitThreshold > 0 && SplitOrders > 1) {
if(NumOpenLong > 0 && !LongExit)
adaptiveExitLong(Cycles, ProfitThreshold);
if(NumOpenShort > 0 && !ShortExit)
adaptiveExitShort(Cycles, ProfitThreshold);
}
// Plots
if(!Train & !is(LOOKBACK)) {
plot("Trend", Trends, MAIN|LINE, BLACK);
if(LongEntry) plotGraph("LongEntry", 0, priceL()*0.99, MAIN|TRIANGLE, GREEN);
if(ShortEntry) plotGraph("ShortEntry", 0, priceH()*1.01, MAIN|TRIANGLE4, RED);
if(CyclePeriod) {
plot("Cycle", Cycles, NEW, RED);
plot("CycleSignal", Cycles+1, 0, colorScale(RED, 70));
plot("Zero", 0, 0, GREY);
plot("Cycle+", 2., 0, GREY);
plot("Cycle-", -2., 0, GREY);
}
// if(RegimePeriod) {
// plot("Regime", Regime, NEW, RED);
// plot("RegimeThreshold", RegimeThreshold, 0, GREY);
// }
// if(AtrStopFactor > 0 && NumOpenTotal > 0) {
// var CurrentStop = 0;
// for(current_trades) {
// CurrentStop = (var)TradeStopLimit;
// break_trades;
// }
// if(CurrentStop > 0) plot("Stop", CurrentStop, MAIN|DOT, BLUE);
// }
}
}
Basic Strategy
Hypothesis
Using Lowpass Filter as the core trend indicator. Compared to traditional moving averages, the lowpass filter better filters market noise while maintaining lower lag. Trading rules are:
- When lowpass filter’s slope turns from negative to positive: close short and go long
- When lowpass filter’s slope turns from positive to negative: close long and go short
This basic signal is expected to capture major structural market turning points, suitable for long-term trend trading.
Test Results
Summary
- Successfully captured major trend reversal points
- Long holding periods resulting in low trading costs
- Prone to consecutive losses in ranging markets
- Experienced 7 consecutive losing trades during the consolidation period of June-November 2024
Optimizing lowpass filter
Hypothesis
The lookback period of the lowpass filter is a key parameter determining signal sensitivity:
- Shorter lookback periods generate more frequent signals
- Longer lookback periods better filter noise
The basic strategy uses a 200-day lookback period. We optimize this parameter to find the optimal range. Parameter scan range: 100–300, step = 10. Considering daily trading characteristics, excessive lookback periods may lead to significant signal lag.
Test Results
As shown below, when the lookback period is within 200–250 range, PRR maintains between 1.4–1.6, showing good stability.
Compared to the basic strategy, optimizing the lowpass filter led to significantly deteriorated results in out-of-sample testing, with PRR dropping from 2.02 to 1.69. There are two possibilities: either the basic strategy’s lookback period was at a performance peak, or the optimization led to overfitting. To test this hypothesis, we adjusted the lookback period values from 100–300 and compared fixed value results with optimization results.
Results are shown in the table below. Only when lookback period=200 did PRR significantly outperform the optimized results, while other tests underperformed the optimized lookback period, indicating that the basic strategy accidentally chose a performance peak, and optimizing the lookback period can indeed improve strategy performance.
Summary
- Strategy performance is stable when lookback period is within 200–250 range
- Optimizing lookback period can significantly improve strategy performance
Pullback Entry
Hypothesis
The basic strategy failed to generate entry signals during the 2017 bull market, revealing limitations of single trend signals. Introducing Fisher Transform as a secondary entry mechanism for the following reasons:
- Fisher Transform effectively identifies cyclical price movements
- Suitable for capturing mid-trend pullback opportunities
- Provides re-entry opportunities when maintaining original trend after stop-loss
Specific rules:
- Long: Trend line rising and Fisher indicator crosses above signal line below zero
- Short: Trend line falling and Fisher indicator crosses below signal line above zero
Test Results
The strategy successfully generated profitable trades during the 2017 bull market. Fisher Transform lookback period is fixed at 10, as the indicator performs stably within 10–20 range, requiring no additional optimization.
Summary
- Significant improvement in equity curve smoothness
- Improved Sharpe ratio and risk-reward ratio
- Successfully addressed the issue of missing important market moves
- Despite slight decreases in profit factor and PRR, the secondary entry mechanism is retained considering overall performance improvement
Market Regime Filter
Hypothesis
Introducing Fractal Dimension to identify market structure and optimize market timing:
- Fractal Dimension > 1.4: Market in ranging state, confirm entry signals
- Fractal Dimension < 1.4: Market in trending state, ignore entry signals
To avoid overfitting, only optimize the Fractal Dimension lookback period (50–200, step = 10), with threshold fixed at 1.4. Threshold typically chosen between 1.3–1.5, as Fractal Dimension effectively identifies market structure within this range.
Test Results
Parameter optimization shows two stable zones: 60–80 and 170–200, but the latter shows higher volatility.
Summary
- PRR decreased from 1.65 to 1.32
- Sharpe ratio dropped from 0.68 to 0.48
- Missed all trades during 2017 and 2020–2021 bull markets
- Although Fractal Dimension accurately identifies market structure, its use as a trade filter actually limited strategy profitability, leading to the decision to abandon this mechanism
Dynamic Exit
Hypothesis
Observed that the strategy only captures 20–40% of trend amplitude in high-volatility markets. Designed a Fisher Transform-based dynamic exit mechanism:
Long position exit rules:
- When unrealized profit exceeds profit threshold (adjusted by ATR)
- Fisher indicator crosses above 2: exit 10%
- Fisher indicator crosses above 3: exit 20%
- Fisher indicator crosses above 4: exit 30%
Short position rules are reversed. The goal is to progressively lock in profits during increased volatility.
Test Results
Key parameters:
- Order split number fixed at 10
- Optimize profit threshold (range 3–20, step = 1)
Parameter optimization results shown below, higher profit threshold correlates with higher PRR in training set.
Summary
- PRR significantly improved to 1.97
- Achieved dynamic profit-taking in long-term trends
- Improved risk-adjusted returns
- Despite suboptimal performance in medium-term ranging markets, mechanism retained due to strong performance in long-term trends
Fixed Stop Loss
Hypothesis
Introducing ATR-based dynamic stop-loss mechanism for risk control. Considering daily timeframe trading characteristics and the existence of re-entry mechanism, stop-loss range should not be too wide.
Parameter settings:
- ATR lookback period fixed at 20 days
- Stop-loss multiplier optimization range: 1–4
- Stop-loss price = Entry price ± (ATR × Stop-loss multiplier)
Test Results
Parameter optimization results shown below, PRR maintains stable when stoploss multiplier in the 2.8–4.0 range.
Summary
- Minor improvements in key metrics: profit factor, PRR, and Sharpe ratio all increased
- Failed to significantly reduce maximum drawdown
- Maximum drawdown duration actually increased
- Despite stop-loss falling short of expectations, mechanism retained for extreme risk protection
Money Management
Adopting fixed risk position sizing for money management:
- Initial capital: 10,000
- Risk per trade: 2%
- Position size = (Account equity × Risk rate) / Stop loss distance
Final strategy equity curve
Strategy performance through iterations
Summary
- Key metrics significantly improved after implementing money management
- Profit factor = 5.02, PRR = 4.47, Sharpe ratio = 1.54, equity curve smoothness (R²) = 97%
- Maximum drawdown = 4.1%, MAE = 9.1%, but longest drawdown period reached 49 weeks
- Maximum consecutive losses of 70, approximately 7 when excluding the effect of dynamic exit order splits
- From a risk-adjusted return perspective, strategy significantly outperforms buy-and-hold, but with modest annual returns (CAGR = 18%)
Robustness Testing
To verify strategy robustness, we conducted robustness testing. The aforementioned analysis used Walk-Forward Analysis (WFA), which involves two key parameters: training set ratio and rolling period. By systematically adjusting these parameters, we can evaluate strategy performance stability in out-of-sample testing. We selected PRR (Pessimistic Return Ratio) as the key evaluation metric; if this metric remains relatively stable across parameter variations, the strategy can be considered robust.
Additionally, robustness testing results can guide the determination of model retraining frequency.
With training set ratio at 75%, as shown in Figure 1, the x-axis represents WFA rolling period count, and y-axis represents PRR. We observe that PRR maintains stability around 2.0 when period count is within 4–9 range.
When training set ratio at 80%, as shown in Figure 2, PRR maintains stability between 1.8–2.0 when period count is within 6–11 range.
Conclusion: The strategy demonstrates stable performance characteristics across different WFA parameter combinations, confirming its robustness. Based on robustness testing results, strategy retraining is recommended for optimal performance.