Skip to content

Commit 56dec4c

Browse files
committed
docs: multi series indicators and custom plotting
1 parent 7c9bcad commit 56dec4c

File tree

3 files changed

+144
-0
lines changed

3 files changed

+144
-0
lines changed

.vitepress/config.mts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ export default defineConfig({
3838
text: 'Indicator Customization',
3939
items: [
4040
{ text: 'Creating a Custom Indicator', link: '/custom-indicator-basics' },
41+
{ text: 'Multi-Series Custom Indicators', link: '/custom-indicator-multi-series' },
42+
{ text: 'Plotting Custom Indicators', link: '/custom-indicator-plotting' },
4143
]
4244
}
4345
],

custom-indicator-multi-series.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Multi-Series Custom Indicators
2+
3+
Many powerful indicators are not just a single line on a chart, but a combination of multiple data series. A classic example is the MACD, which consists of the MACD line, a signal line, and a histogram.
4+
5+
Stochastix allows your custom indicators to compute and expose multiple, distinct `Series` objects. This is achieved by using the `$this->resultSeries` property as an associative array.
6+
7+
## The `$resultSeries` Property
8+
9+
As we saw on the previous page, the final step in the `calculateBatch()` method is to store your calculated data in the `$this->resultSeries` property.
10+
11+
This property is an associative array where:
12+
* **The key** is a `string` that uniquely identifies a specific data series (e.g., `'rsi'`, `'rsi_ma'`).
13+
* **The value** is the final, padded `Series` object for that data.
14+
15+
By adding multiple key-value pairs to this array, your indicator effectively publishes multiple output series.
16+
17+
## A Practical Example: `RSIMovingAverage`
18+
19+
Let's revisit the `RSIMovingAverage` indicator we created. It naturally produces two distinct data series: the RSI line itself, and the moving average of that RSI line.
20+
21+
In the `calculateBatch()` method, we stored them with unique keys:
22+
23+
```php
24+
// In src/Indicator/RSIMovingAverage.php
25+
26+
public function calculateBatch(Map $dataframes): void
27+
{
28+
// ... (calculations for $rsiPadded and $maPadded)
29+
30+
// Store the final, padded data as Series objects.
31+
// The keys used here are critical for accessing the data later.
32+
$this->resultSeries['rsi'] = new Series($rsiPadded);
33+
$this->resultSeries['rsi_ma'] = new Series($maPadded);
34+
}
35+
```
36+
37+
## Accessing Multi-Value Series in a Strategy
38+
39+
When you want to use this indicator in a strategy, you access each series by providing the specific key as the second argument to the `$this->getIndicatorSeries()` method.
40+
41+
```php
42+
// In your strategy's onBar() method:
43+
44+
// Get the indicator instance we defined earlier.
45+
$indicatorKey = 'my_custom_rsi';
46+
47+
// To get the raw RSI line, we use the 'rsi' key.
48+
$rsiLine = $this->getIndicatorSeries($indicatorKey, 'rsi');
49+
50+
// To get the moving average line, we use the 'rsi_ma' key.
51+
$rsiMaLine = $this->getIndicatorSeries($indicatorKey, 'rsi_ma');
52+
53+
// Now we can use both series in our logic.
54+
if ($rsiLine->crossesOver($rsiMaLine)) {
55+
// A bullish signal has occurred.
56+
// ...
57+
}
58+
```
59+
60+
::: warning Important
61+
If you do not provide the second argument to `$this->getIndicatorSeries()`, it will default to requesting a series with the key `'value'`. In the case of our `RSIMovingAverage` indicator, this would cause an error because we did not define a series with the key `'value'`. You must always use the explicit keys you defined in your indicator's `calculateBatch()` method.
62+
:::

custom-indicator-plotting.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Plotting Custom Indicators
2+
3+
Once you've created a custom indicator, you'll want to see it on the results chart to analyze its behavior. Stochastix provides a clean way for an indicator to define its own default plotting configuration. This makes your custom indicators reusable and easy to visualize without adding complex plotting logic to your strategies.
4+
5+
This is achieved by implementing the `getPlotDefinition()` method in your indicator class.
6+
7+
## The `getPlotDefinition()` Method
8+
9+
By implementing this method from the `IndicatorInterface`, your custom indicator can return a `PlotDefinition` object that acts as a "template" for how it should be drawn on a chart.
10+
11+
The backtesting framework can then use this template automatically whenever you add the indicator to a strategy.
12+
13+
## Implementing the Plot for `RSIMovingAverage`
14+
15+
Let's continue with our `RSIMovingAverage` example and implement its `getPlotDefinition()` method. We want to display the RSI and its moving average in a separate pane below the price chart, with horizontal lines at the 70 and 30 levels.
16+
17+
```php
18+
// In src/Indicator/RSIMovingAverage.php
19+
20+
use Stochastix\Domain\Plot\Annotation\HorizontalLine;
21+
use Stochastix\Domain\Plot\Enum\HorizontalLineStyleEnum;
22+
use Stochastix\Domain\Plot\PlotDefinition;
23+
use Stochastix\Domain\Plot\Series\Line;
24+
25+
// ... inside the RSIMovingAverage class
26+
27+
public function getPlotDefinition(): ?PlotDefinition
28+
{
29+
return new PlotDefinition(
30+
// This name is a default suggestion; it can be overridden by the strategy.
31+
name: 'RSI / MA',
32+
33+
// 'false' renders the plot in a separate pane below the price chart.
34+
overlay: false,
35+
36+
// Define the data series to draw.
37+
plots: [
38+
// Draw a line using the data from the 'rsi' series key.
39+
new Line(key: 'rsi', color: '#4e79a7'),
40+
41+
// Draw a second line using data from the 'rsi_ma' series key.
42+
new Line(key: 'rsi_ma', color: '#f28e2b'),
43+
],
44+
45+
// Define static annotations for the plot pane.
46+
annotations: [
47+
new HorizontalLine(value: 70, style: HorizontalLineStyleEnum::Dashed),
48+
new HorizontalLine(value: 30, style: HorizontalLineStyleEnum::Dashed),
49+
]
50+
);
51+
}
52+
```
53+
54+
**Key Concepts from this step:**
55+
* **`plots` array**: We define one `Line` for each data series we stored in `$this->resultSeries`. The `key` in the `Line` constructor **must match** the key used in `calculateBatch()` (e.g., `'rsi'`, `'rsi_ma'`).
56+
* **`annotations` array**: We add static `HorizontalLine` components to provide context for the overbought (70) and oversold (30) RSI levels.
57+
* **`overlay: false`**: This is critical for oscillators like RSI, ensuring they get their own pane and don't get drawn on top of the price candles.
58+
59+
## Using the Self-Plotting Indicator
60+
61+
Now that our `RSIMovingAverage` indicator knows how to draw itself, using it in a strategy becomes incredibly simple.
62+
63+
To activate the automatic plotting, you provide a third argument to the `$this->addIndicator()` method: the desired name for the plot in the chart legend. When this third argument is present, the framework automatically calls your indicator's `getPlotDefinition()` method and uses the template it provides.
64+
65+
```php
66+
// In your strategy's defineIndicators() method:
67+
68+
use App\Indicator\RSIMovingAverage;
69+
70+
protected function defineIndicators(): void
71+
{
72+
$this->addIndicator(
73+
'my_rsi', // The key for the indicator instance
74+
new RSIMovingAverage(rsiPeriod: 14, maPeriod: 9),
75+
'RSI (14) with MA (9)' // This 3rd argument activates the plot!
76+
);
77+
}
78+
```
79+
80+
That's it! With this single line, you have added your custom indicator's logic to the backtest *and* configured it to be displayed beautifully on the results chart, with no plotting code needed in the strategy itself.

0 commit comments

Comments
 (0)