|
| 1 | +# Multi-Timeframe Strategies |
| 2 | + |
| 3 | +Analyzing a single timeframe can give you entry signals, but professional strategies often gain a significant edge by consulting higher timeframes to determine the overall market trend. This is known as Multi-Timeframe (MTF) Analysis. |
| 4 | + |
| 5 | +Stochastix has first-class support for MTF analysis, allowing your strategy to seamlessly access data from different candle sizes within a single `onBar()` execution. |
| 6 | + |
| 7 | +::: warning Data must be downloaded |
| 8 | +Before running an MTF strategy, you **must** download the historical data for **every timeframe** you intend to use. If a secondary timeframe's data file is missing, the backtest will fail. |
| 9 | + |
| 10 | +Please refer to the [**Downloading Market Data**](/data-downloading) guide for instructions. |
| 11 | +::: |
| 12 | + |
| 13 | +## 1. Requesting Timeframe Data |
| 14 | + |
| 15 | +The first step is to inform the backtesting engine which timeframes your strategy requires. This is done using the `requiredMarketData` parameter in the `#[AsStrategy]` attribute. |
| 16 | + |
| 17 | +Let's design a simple strategy that trades on a 1-hour (`H1`) chart but uses the 4-hour (`H4`) chart to confirm the trend. |
| 18 | + |
| 19 | +```php |
| 20 | +use Stochastix\Domain\Common\Enum\TimeframeEnum; |
| 21 | +use Stochastix\Domain\Strategy\Attribute\AsStrategy; |
| 22 | + |
| 23 | +#[AsStrategy( |
| 24 | + alias: 'mtf_rsi_trend', |
| 25 | + name: 'Multi-Timeframe RSI Trend', |
| 26 | + // The primary timeframe for the strategy |
| 27 | + timeframe: TimeframeEnum::H1, |
| 28 | + // An array of all timeframes the strategy needs access to. |
| 29 | + requiredMarketData: [ |
| 30 | + TimeframeEnum::H1, |
| 31 | + TimeframeEnum::H4, |
| 32 | + ] |
| 33 | +)] |
| 34 | +class MtfRsiStrategy extends AbstractStrategy |
| 35 | +{ |
| 36 | + // ... |
| 37 | +} |
| 38 | +``` |
| 39 | + |
| 40 | +## 2. Accessing Timeframe Data in `onBar` |
| 41 | + |
| 42 | +When you request secondary data, the `onBar()` method receives a `MultiTimeframeOhlcvSeries` object as its `$bars` parameter. This special object provides access to all requested timeframes. |
| 43 | + |
| 44 | +### Accessing the Primary Timeframe |
| 45 | + |
| 46 | +You access the primary timeframe's data (in this case, `H1`) directly via the object's properties: |
| 47 | + |
| 48 | +```php |
| 49 | +// Get the most recent close price from the primary H1 series |
| 50 | +$h1_close = $bars->close[0]; |
| 51 | + |
| 52 | +// Get the previous open price from the primary H1 series |
| 53 | +$h1_previous_open = $bars->open[1]; |
| 54 | +``` |
| 55 | + |
| 56 | +### Accessing a Secondary Timeframe |
| 57 | + |
| 58 | +You access a secondary timeframe's data by treating the `$bars` object like an array, using the `TimeframeEnum` case as the key. This returns a complete `OhlcvSeries` object for that timeframe, or `null` if the data isn't available yet. |
| 59 | + |
| 60 | +```php |
| 61 | +public function onBar(MultiTimeframeOhlcvSeries $bars): void |
| 62 | +{ |
| 63 | + // Access the entire 4-hour OhlcvSeries object |
| 64 | + $h4_bars = $bars[TimeframeEnum::H4]; |
| 65 | + |
| 66 | + // It's crucial to check if the data exists, as the higher timeframe |
| 67 | + // may not have a bar corresponding to the current primary bar. |
| 68 | + if ($h4_bars === null) { |
| 69 | + return; |
| 70 | + } |
| 71 | + |
| 72 | + // Now you can access the H4 data just like a normal series. |
| 73 | + // The engine ensures the data is correctly aligned in time. |
| 74 | + $h4_close = $h4_bars->close[0]; |
| 75 | + $h4_high = $h4_bars->high[0]; |
| 76 | + |
| 77 | + // ... your logic using both H1 and H4 data |
| 78 | +} |
| 79 | +``` |
| 80 | + |
| 81 | +## 3. A Complete Example |
| 82 | + |
| 83 | +Let's build a full strategy that uses our custom `RSIMovingAverage` indicator on the 1H chart and a simple SMA on the 4H chart as a trend filter. |
| 84 | + |
| 85 | +**The Logic:** |
| 86 | + |
| 87 | + * **Primary Timeframe (1H):** We will use the `RSIMovingAverage` indicator to find entry signals. |
| 88 | + * **Secondary Timeframe (4H):** We will use a 50-period SMA to determine the long-term trend. |
| 89 | + * **Entry Condition:** We will only enter a `long` position if the 1H RSI crosses above its moving average **AND** the current 1H price is above the 4H SMA(50). |
| 90 | + * **Exit Condition:** We will exit when the 1H RSI crosses back below its moving average. |
| 91 | + |
| 92 | +Here is the full strategy code: |
| 93 | + |
| 94 | +```php |
| 95 | +<?php |
| 96 | + |
| 97 | +namespace App\Strategy; |
| 98 | + |
| 99 | +use App\Indicator\RSIMovingAverage; |
| 100 | +use Stochastix\Domain\Common\Enum\DirectionEnum; |
| 101 | +use Stochastix\Domain\Common\Enum\TimeframeEnum; |
| 102 | +use Stochastix\Domain\Common\Enum\TALibFunctionEnum; |
| 103 | +use Stochastix\Domain\Common\Model\MultiTimeframeOhlcvSeries; |
| 104 | +use Stochastix\Domain\Indicator\Model\TALibIndicator; |
| 105 | +use Stochastix\Domain\Order\Enum\OrderTypeEnum; |
| 106 | +use Stochastix\Domain\Strategy\AbstractStrategy; |
| 107 | +use Stochastix\Domain\Strategy\Attribute\AsStrategy; |
| 108 | +use Stochastix\Domain\Strategy\Attribute\Input; |
| 109 | + |
| 110 | +#[AsStrategy( |
| 111 | + alias: 'mtf_rsi_trend', |
| 112 | + name: 'Multi-Timeframe RSI Trend', |
| 113 | + timeframe: TimeframeEnum::H1, |
| 114 | + requiredMarketData: [TimeframeEnum::H1, TimeframeEnum::H4] |
| 115 | +)] |
| 116 | +class MtfRsiStrategy extends AbstractStrategy |
| 117 | +{ |
| 118 | + #[Input(description: 'RSI Period on the primary timeframe')] |
| 119 | + private int $rsiPeriod = 14; |
| 120 | + |
| 121 | + #[Input(description: 'MA Period for the RSI line')] |
| 122 | + private int $rsiMaPeriod = 9; |
| 123 | + |
| 124 | + #[Input(description: 'SMA Period for the higher timeframe trend filter')] |
| 125 | + private int $trendSmaPeriod = 50; |
| 126 | + |
| 127 | + protected function defineIndicators(): void |
| 128 | + { |
| 129 | + // Indicator for our primary (H1) timeframe |
| 130 | + $this->addIndicator( |
| 131 | + 'h1_rsi_ma', |
| 132 | + new RSIMovingAverage($this->rsiPeriod, $this->rsiMaPeriod) |
| 133 | + ); |
| 134 | + |
| 135 | + // Indicator for our secondary (H4) timeframe trend filter |
| 136 | + $this->addIndicator( |
| 137 | + 'h4_trend_sma', |
| 138 | + new TALibIndicator( |
| 139 | + TALibFunctionEnum::Sma, |
| 140 | + ['timePeriod' => $this->trendSmaPeriod], |
| 141 | + sourceTimeframe: TimeframeEnum::H4 // Tell the indicator to use H4 data |
| 142 | + ) |
| 143 | + ); |
| 144 | + } |
| 145 | + |
| 146 | + public function onBar(MultiTimeframeOhlcvSeries $bars): void |
| 147 | + { |
| 148 | + // --- 1. Get indicator values --- |
| 149 | + $h1Rsi = $this->getIndicatorSeries('h1_rsi_ma', 'rsi'); |
| 150 | + $h1RsiMa = $this->getIndicatorSeries('h1_rsi_ma', 'rsi_ma'); |
| 151 | + $h4Sma = $this->getIndicatorSeries('h4_trend_sma'); |
| 152 | + |
| 153 | + // --- 2. Check for missing data --- |
| 154 | + // The higher timeframe data or indicators might not have values yet. |
| 155 | + if ($h1Rsi[0] === null || $h1RsiMa[0] === null || $h4Sma[0] === null) { |
| 156 | + return; |
| 157 | + } |
| 158 | + |
| 159 | + // --- 3. Define the conditions --- |
| 160 | + $isBullishTrend = $bars->close[0] > $h4Sma[0]; |
| 161 | + $isEntrySignal = $h1Rsi->crossesOver($h1RsiMa); |
| 162 | + $isExitSignal = $h1Rsi->crossesUnder($h1RsiMa); |
| 163 | + |
| 164 | + // --- 4. Execute trading logic --- |
| 165 | + if (!$this->isInPosition()) { |
| 166 | + // Only enter long if our H1 signal occurs during a H4 uptrend. |
| 167 | + if ($isEntrySignal && $isBullishTrend) { |
| 168 | + $this->entry(DirectionEnum::Long, OrderTypeEnum::Market, '0.1'); |
| 169 | + } |
| 170 | + } else { |
| 171 | + if ($isExitSignal) { |
| 172 | + $this->exit('0.1'); |
| 173 | + } |
| 174 | + } |
| 175 | + } |
| 176 | +} |
| 177 | +``` |
0 commit comments