Skip to content

Commit 7c9bcad

Browse files
committed
docs: create custom indicator
1 parent 4eb7832 commit 7c9bcad

File tree

2 files changed

+163
-0
lines changed

2 files changed

+163
-0
lines changed

.vitepress/config.mts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ export default defineConfig({
3333
{ text: 'Position Sizing', link: '/position-sizing' },
3434
{ text: 'Plotting & Visualization', link: '/plotting' },
3535
]
36+
},
37+
{
38+
text: 'Indicator Customization',
39+
items: [
40+
{ text: 'Creating a Custom Indicator', link: '/custom-indicator-basics' },
41+
]
3642
}
3743
],
3844

custom-indicator-basics.md

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
# Creating a Custom Indicator
2+
3+
While Stochastix provides a powerful `TALibIndicator` wrapper that covers dozens of standard indicators, you may eventually need to create your own. You might do this to:
4+
* Implement a proprietary indicator that doesn't exist in TA-Lib.
5+
* Create a composite indicator that combines the logic of several others.
6+
* Perform custom mathematical transformations on existing indicator data.
7+
8+
This guide will walk you through creating a new custom indicator from scratch. We will build a simple but useful indicator called `RSIMovingAverage`, which calculates the Relative Strength Index (RSI) and then calculates a Simple Moving Average (SMA) of the RSI line itself.
9+
10+
## The Core Structure
11+
12+
Every indicator in Stochastix must implement the `IndicatorInterface`. However, the easiest way to start is by extending the `AbstractIndicator` base class, which provides some helpful boilerplate.
13+
14+
Custom indicators should be placed in the `src/Indicator/` directory of your project as convention, although it's not mandatory.
15+
16+
## Building the `RSIMovingAverage` Indicator
17+
18+
### Step 1: Create the Class File
19+
20+
First, create a new file at `src/Indicator/RSIMovingAverage.php`.
21+
22+
The basic structure will include a constructor to accept parameters (like the RSI and SMA periods) and the required `calculateBatch()` method.
23+
24+
```php
25+
<?php
26+
27+
namespace App\Indicator;
28+
29+
use Ds\Map;
30+
use Stochastix\Domain\Common\Enum\AppliedPriceEnum;
31+
use Stochastix\Domain\Indicator\Model\AbstractIndicator;
32+
33+
final class RSIMovingAverage extends AbstractIndicator
34+
{
35+
public function __construct(
36+
private readonly int $rsiPeriod,
37+
private readonly int $maPeriod,
38+
private readonly AppliedPriceEnum $source = AppliedPriceEnum::Close
39+
) {
40+
}
41+
42+
public function calculateBatch(Map $dataframes): void
43+
{
44+
// Calculation logic will go here
45+
}
46+
}
47+
```
48+
49+
### Step 2: Implement the `calculateBatch()` Method
50+
51+
This method is the heart of your indicator. It receives all the market data and is responsible for performing calculations and storing the results.
52+
53+
```php
54+
// Inside the RSIMovingAverage class
55+
56+
use Ds\Map;
57+
use Stochastix\Domain\Common\Model\Series;
58+
59+
public function calculateBatch(Map $dataframes): void
60+
{
61+
// 1. Get the source price series (e.g., close prices) from the primary dataframe.
62+
$sourceSeries = $dataframes->get('primary')[$this->source->value];
63+
64+
if ($sourceSeries->isEmpty()) {
65+
return; // Not enough data to calculate.
66+
}
67+
68+
$inputData = $sourceSeries->toArray();
69+
$inputCount = count($inputData);
70+
71+
// 2. Calculate the RSI.
72+
// The result is an array shorter than the input due to the warmup period.
73+
$rsiResult = trader_rsi($inputData, $this->rsiPeriod);
74+
if ($rsiResult === false) {
75+
return; // Calculation failed.
76+
}
77+
78+
// We must pad the result with nulls to match the original input count for time alignment.
79+
$rsiPadded = $this->createPaddedArray($rsiResult, $inputCount);
80+
81+
// 3. Calculate the SMA of the RSI.
82+
// Note: We use the *padded* RSI data as input here. trader_* functions can handle nulls.
83+
$maResult = trader_sma($rsiPadded, $this->maPeriod);
84+
if ($maResult === false) {
85+
return;
86+
}
87+
$maPadded = $this->createPaddedArray($maResult, $inputCount);
88+
89+
// 4. Store the final, padded data as Series objects in the resultSeries property.
90+
// The keys 'rsi' and 'rsi_ma' are how we will access them later.
91+
$this->resultSeries['rsi'] = new Series($rsiPadded);
92+
$this->resultSeries['rsi_ma'] = new Series($maPadded);
93+
}
94+
95+
/**
96+
* Creates a new array from a trader function result, padding it with nulls
97+
* at the beginning to match the original input data count.
98+
*/
99+
private function createPaddedArray(array|false $traderResult, int $expectedCount): array
100+
{
101+
if ($traderResult === false) {
102+
return array_fill(0, $expectedCount, null);
103+
}
104+
105+
$outputCount = count($traderResult);
106+
$paddingCount = $expectedCount - $outputCount;
107+
108+
return ($paddingCount > 0)
109+
? array_merge(array_fill(0, $paddingCount, null), array_values($traderResult))
110+
: array_values($traderResult);
111+
}
112+
```
113+
**Key Concepts from this step:**
114+
115+
* **Warm-up Periods**: `trader` functions return shorter arrays than their input because they need a "warm-up" period (e.g., an SMA(20) can't produce a value until it has 20 data points).
116+
* **Padding**: To ensure correct time alignment, we must pad the start of the result arrays with `null`s to match the count of the original source data. The `createPaddedArray` helper function handles this.
117+
* **Storing Results**: The final, correctly-sized arrays are wrapped in `Series` objects and stored in the `$this->resultSeries` property. This makes them available to your strategy.
118+
119+
### Step 3: Using the Custom Indicator
120+
121+
Now your custom indicator is complete and can be used in any strategy just like the built-in `TALibIndicator`.
122+
123+
In your strategy's `defineIndicators()` method:
124+
125+
```php
126+
// In a strategy file, e.g., src/Strategy/MyCustomRsiStrategy.php
127+
128+
use App\Indicator\RSIMovingAverage; // Import your new class
129+
130+
// ...
131+
132+
protected function defineIndicators(): void
133+
{
134+
$this->addIndicator(
135+
'my_rsi', // A unique key for this indicator instance
136+
new RSIMovingAverage(rsiPeriod: 14, maPeriod: 9)
137+
);
138+
}
139+
```
140+
141+
And in your `onBar()` method, you can access its outputs:
142+
143+
```php
144+
public function onBar(MultiTimeframeOhlcvSeries $bars): void
145+
{
146+
// Access the 'rsi' series from your custom indicator
147+
$rsiLine = $this->getIndicatorSeries('my_rsi', 'rsi');
148+
149+
// Access the 'rsi_ma' series
150+
$rsiMaLine = $this->getIndicatorSeries('my_rsi', 'rsi_ma');
151+
152+
// Example logic
153+
if ($rsiLine->crossesOver($rsiMaLine)) {
154+
// ... RSI has crossed above its moving average
155+
}
156+
}
157+
```

0 commit comments

Comments
 (0)