Skip to content

Commit 634bc2c

Browse files
committed
docs: anatomy of a strategy
1 parent 8d8924b commit 634bc2c

File tree

2 files changed

+138
-0
lines changed

2 files changed

+138
-0
lines changed

.vitepress/config.mts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ export default defineConfig({
2323
{ text: 'Core Concepts', link: '/core-concepts' },
2424
{ text: 'Configuration', link: '/configuration' },
2525
]
26+
},
27+
{
28+
text: 'Writing strategies',
29+
items: [
30+
{ text: 'Anatomy of a Strategy', link: '/anatomy' },
31+
]
2632
}
2733
],
2834

anatomy.md

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
# Anatomy of a Strategy
2+
3+
Every trading strategy in Stochastix is a PHP class. While you could implement the `StrategyInterface` directly, the recommended and most convenient approach is to extend the `Stochastix\Domain\Strategy\AbstractStrategy` class. This base class provides a structured lifecycle and a rich set of helper methods for interacting with the backtesting engine.
4+
5+
This page will break down the essential components and lifecycle of a standard strategy class.
6+
7+
## The `#[AsStrategy]` Attribute
8+
9+
For Stochastix to recognize your class as a runnable strategy, you must decorate it with the `#[AsStrategy]` PHP attribute. This attribute registers your strategy with the framework, making it available in the user interface and on the command line.
10+
11+
It takes the following parameters:
12+
13+
* `alias`: A unique, snake_case string used to identify the strategy on the CLI (e.g., `ema_crossover`).
14+
* `name`: A human-readable name for display in the UI (e.g., "EMA Crossover").
15+
* `description`: An optional, longer description of what the strategy does.
16+
17+
```php
18+
<?php
19+
20+
namespace App\Strategy;
21+
22+
use Stochastix\Domain\Strategy\AbstractStrategy;
23+
use Stochastix\Domain\Strategy\Attribute\AsStrategy;
24+
25+
#[AsStrategy(
26+
alias: 'ema_crossover',
27+
name: 'EMA Crossover Strategy',
28+
description: 'A simple strategy that trades on the crossover of two Exponential Moving Averages.'
29+
)]
30+
class MyEmaStrategy extends AbstractStrategy
31+
{
32+
// ...
33+
}
34+
```
35+
36+
## Defining Inputs with `#[Input]`
37+
38+
To make your strategy flexible and configurable, you can expose parameters using the `#[Input]` attribute on class properties. Any property decorated with this attribute will automatically become a configurable input in the UI and an option on the CLI.
39+
40+
You can define a description and a default value for each input directly in the property declaration.
41+
42+
```php
43+
use Stochastix\Domain\Strategy\Attribute\Input;
44+
45+
#[AsStrategy(alias: 'ema_crossover', name: 'EMA Crossover Strategy')]
46+
class MyEmaStrategy extends AbstractStrategy
47+
{
48+
#[Input(description: 'Period for the fast EMA', min: 1)]
49+
private int $emaFastPeriod = 12;
50+
51+
#[Input(description: 'Period for the slow EMA', min: 1)]
52+
private int $emaSlowPeriod = 26;
53+
54+
// ...
55+
}
56+
```
57+
58+
## The Strategy Lifecycle
59+
60+
When a backtest runs, the framework calls several methods on your strategy class in a specific order. Understanding this lifecycle is key to writing effective strategies.
61+
62+
### 1. `afterConfigured()`
63+
64+
This is an optional hook method that you can implement. It is called *after* all the `#[Input]` parameters have been resolved and set on your strategy object, but *before* the main initialization and indicator setup. Most strategies will not need this method, but it is available for advanced setup logic that depends on the final configuration values.
65+
66+
```php
67+
protected function afterConfigured(): void
68+
{
69+
// Optional: Perform actions after inputs are configured.
70+
// For example, validate that emaFastPeriod is less than emaSlowPeriod.
71+
if ($this->emaFastPeriod >= $this->emaSlowPeriod) {
72+
throw new \InvalidArgumentException('Fast EMA period must be less than Slow EMA period.');
73+
}
74+
}
75+
```
76+
77+
### 2. `defineIndicators()`
78+
79+
This is a required abstract method that you **must** implement. It is called once per symbol at the beginning of the backtest. Its sole purpose is to define all the indicators your strategy will need for its calculations.
80+
81+
This is where you will use the `$this->addIndicator()` helper method.
82+
83+
```php
84+
use Stochastix\Domain\Common\Enum\TALibFunctionEnum;
85+
use Stochastix\Domain\Indicator\Model\TALibIndicator;
86+
87+
// ... inside your strategy class
88+
89+
protected function defineIndicators(): void
90+
{
91+
$this
92+
->addIndicator(
93+
'ema_fast',
94+
new TALibIndicator(TALibFunctionEnum::Ema, ['timePeriod' => $this->emaFastPeriod])
95+
)
96+
->addIndicator(
97+
'ema_slow',
98+
new TALibIndicator(TALibFunctionEnum::Ema, ['timePeriod' => $this->emaSlowPeriod])
99+
);
100+
}
101+
```
102+
103+
### 3. `onBar()`
104+
105+
This abstract method is the heart of your strategy and **must** be implemented. It is called for every single candle (or "bar") in the dataset, from start to finish.
106+
107+
All of your core trading logic resides here:
108+
* Accessing indicator values for the current bar.
109+
* Checking for entry or exit conditions.
110+
* Placing new orders.
111+
112+
```php
113+
use Stochastix\Domain\Common\Model\MultiTimeframeOhlcvSeries;
114+
115+
// ... inside your strategy class
116+
117+
public function onBar(MultiTimeframeOhlcvSeries $bars): void
118+
{
119+
$fastEma = $this->getIndicatorSeries('ema_fast');
120+
$slowEma = $this->getIndicatorSeries('ema_slow');
121+
122+
// Check for a long entry signal
123+
if ($fastEma->crossesOver($slowEma)) {
124+
// ... place a long order
125+
}
126+
127+
// Check for a short entry signal
128+
if ($fastEma->crossesUnder($slowEma)) {
129+
// ... place a short order
130+
}
131+
}
132+
```

0 commit comments

Comments
 (0)