Skip to content

Commit 332b7b0

Browse files
committed
feat: improve recipe with a sample strategy
1 parent 3d63b84 commit 332b7b0

File tree

5 files changed

+237
-46
lines changed

5 files changed

+237
-46
lines changed

.editorconfig

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
root = true
2+
3+
[*]
4+
charset = utf-8
5+
end_of_line = lf
6+
insert_final_newline = true
7+
trim_trailing_whitespace = true
8+
9+
[*.php]
10+
indent_style = space
11+
indent_size = 4
12+
13+
[*.yml]
14+
indent_style = space
15+
indent_size = 4
16+
17+
[*.yaml]
18+
indent_style = space
19+
indent_size = 4
20+
21+
[*.md]
22+
trim_trailing_whitespace = false
23+
24+
[.github/workflows/*.yaml]
25+
indent_style = space
26+
indent_size = 2
27+
28+
[cook.yaml]
29+
indent_style = space
30+
indent_size = 2

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
"symfony/doctrine-messenger": "^7.3",
4444
"symfony/orm-pack": "^2.4",
4545
"symfony/cache": "^7.3",
46-
"williarin/cook": "^1.3"
46+
"williarin/cook": "^2.0"
4747
},
4848
"require-dev": {
4949
"roave/security-advisories": "dev-latest",

composer.lock

Lines changed: 33 additions & 35 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cook.yaml

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
11
files:
2-
Makefile: recipe/Makefile
2+
Makefile:
3+
if_exists: ignore
4+
source: recipe/Makefile
35

4-
.env:
5-
content: |-
6-
DATABASE_URL="sqlite:///%kernel.project_dir%/data/queue_%kernel.environment%.db"
6+
.env:
7+
type: env
8+
if_exists: comment
9+
content: |-
10+
DATABASE_URL="sqlite:///%kernel.project_dir%/data/queue_%kernel.environment%.db"
711
812
directories:
9-
'%ROOT_DIR%/data/': recipe/data/
10-
'%CONFIG_DIR%/': recipe/config/
13+
'%ROOT_DIR%/data/': recipe/data/
14+
'%CONFIG_DIR%/': recipe/config/
15+
'%SRC_DIR%/': recipe/src/
1116

1217
post_install_output: |
13-
<bg=blue;fg=white> </>
14-
<bg=blue;fg=white> What's next? </>
15-
<bg=blue;fg=white> </>
18+
<bg=blue;fg=white> </>
19+
<bg=blue;fg=white> What's next? </>
20+
<bg=blue;fg=white> </>
1621
17-
* <fg=blue>Read</> the full documentation at <comment>https://phpquant.github.io/stochastix-docs</>
22+
* <fg=blue>Read</> the full documentation at <comment>https://phpquant.github.io/stochastix-docs</>
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
<?php
2+
3+
namespace App\Strategy;
4+
5+
use Psr\Log\LoggerInterface;
6+
use Stochastix\Domain\Common\Enum\DirectionEnum;
7+
use Stochastix\Domain\Common\Enum\TALibFunctionEnum;
8+
use Stochastix\Domain\Common\Model\OhlcvSeries;
9+
use Stochastix\Domain\Indicator\Model\TALibIndicator;
10+
use Stochastix\Domain\Order\Enum\OrderTypeEnum;
11+
use Stochastix\Domain\Plot\Series\Line;
12+
use Stochastix\Domain\Strategy\AbstractStrategy;
13+
use Stochastix\Domain\Strategy\Attribute\AsStrategy;
14+
use Stochastix\Domain\Strategy\Attribute\Input;
15+
16+
#[AsStrategy(alias: 'sample_strategy', name: 'EMA Crossover')]
17+
final class SampleStrategy extends AbstractStrategy
18+
{
19+
#[Input(description: 'Period for the fast EMA', min: 1)]
20+
private int $emaFastPeriod = 12;
21+
22+
#[Input(description: 'Period for the slow EMA', min: 1)]
23+
private int $emaSlowPeriod = 26;
24+
25+
#[Input(description: 'Stop-loss percentage', min: 0.001, max: 0.5)]
26+
private float $stopLossPercentage = 0.02;
27+
28+
#[Input(description: 'Stake amount as a percentage of capital', min: 0.001, max: 1.0)]
29+
private float $stakeAmount = 0.02;
30+
31+
public function __construct(private readonly LoggerInterface $logger)
32+
{
33+
}
34+
35+
protected function defineIndicators(): void
36+
{
37+
$this
38+
->addIndicator(
39+
'ema_fast',
40+
new TALibIndicator(TALibFunctionEnum::Ema, ['timePeriod' => $this->emaFastPeriod])
41+
)
42+
->addIndicator(
43+
'ema_slow',
44+
new TALibIndicator(TALibFunctionEnum::Ema, ['timePeriod' => $this->emaSlowPeriod])
45+
)
46+
// ->addIndicator(
47+
// 'macd',
48+
// new TALibIndicator(TALibFunctionEnum::Macd, [
49+
// 'fastPeriod' => 12,
50+
// 'slowPeriod' => 26,
51+
// 'signalPeriod' => 9
52+
// ])
53+
// )
54+
->definePlot(
55+
indicatorKey: 'ema_fast',
56+
name: "EMA ($this->emaFastPeriod)",
57+
overlay: true,
58+
plots: [
59+
new Line(color: '#4e79a7'),
60+
]
61+
)
62+
->definePlot(
63+
indicatorKey: 'ema_slow',
64+
name: "EMA ($this->emaSlowPeriod)",
65+
overlay: true,
66+
plots: [
67+
new Line(color: '#f28e2b'),
68+
]
69+
)
70+
// ->definePlot(
71+
// indicatorKey: 'macd',
72+
// name: 'MACD (12, 26, 9)',
73+
// overlay: false,
74+
// plots: [
75+
// new Line(key: 'macd', color: '#2962FF'),
76+
// new Line(key: 'signal', color: '#FF6D00'),
77+
// new Histogram(key: 'hist', color: 'rgba(178, 181, 190, 0.5)'),
78+
// ],
79+
// annotations: [
80+
// new HorizontalLine(value: 0, color: '#787b86', style: HorizontalLineStyleEnum::Dashed)
81+
// ]
82+
// )
83+
;
84+
}
85+
86+
public function onBar(OhlcvSeries $bars): void
87+
{
88+
$currentSymbol = $this->context->getCurrentSymbol();
89+
$currentClose = $bars->close[0];
90+
91+
if ($currentClose === null) {
92+
return;
93+
}
94+
95+
$fastEma = $this->getIndicatorSeries('ema_fast');
96+
$slowEma = $this->getIndicatorSeries('ema_slow');
97+
98+
if ($fastEma[0] === null || $slowEma[0] === null) {
99+
return;
100+
}
101+
102+
$isUpwardCross = $fastEma->crossesOver($slowEma);
103+
$isDownwardCross = $fastEma->crossesUnder($slowEma);
104+
105+
$currentCloseStr = (string) $currentClose;
106+
107+
if (!$this->isInPosition()) {
108+
$availableCash = $this->orderManager->getPortfolioManager()->getAvailableCash();
109+
$stakeInCash = bcmul($availableCash, (string) $this->stakeAmount);
110+
111+
if (bccomp($currentCloseStr, '0') <= 0) {
112+
$this->logger->warning('Current close price is zero or negative, cannot calculate quantity.');
113+
114+
return;
115+
}
116+
$tradeQuantity = bcdiv($stakeInCash, $currentCloseStr);
117+
118+
if (bccomp($tradeQuantity, '0.00000001') < 0) {
119+
$this->logger->info('Calculated trade quantity ({qty}) too small with cash {cash}, skipping trade.', [
120+
'qty' => $tradeQuantity,
121+
'cash' => $availableCash,
122+
]);
123+
124+
return;
125+
}
126+
127+
if ($isUpwardCross) {
128+
$slFactor = bcsub('1', (string) $this->stopLossPercentage);
129+
$stopLossPrice = bcmul($currentCloseStr, $slFactor);
130+
$this->entry(
131+
direction: DirectionEnum::Long,
132+
orderType: OrderTypeEnum::Market,
133+
quantity: $tradeQuantity,
134+
stopLossPrice: $stopLossPrice,
135+
);
136+
} elseif ($isDownwardCross) {
137+
$slFactor = bcadd('1', (string) $this->stopLossPercentage);
138+
$stopLossPrice = bcmul($currentCloseStr, $slFactor);
139+
$this->entry(
140+
direction: DirectionEnum::Short,
141+
orderType: OrderTypeEnum::Market,
142+
quantity: $tradeQuantity,
143+
stopLossPrice: $stopLossPrice,
144+
);
145+
}
146+
} else {
147+
$openPosition = $this->orderManager->getPortfolioManager()->getOpenPosition($currentSymbol);
148+
149+
if ($openPosition) {
150+
if ($openPosition->direction === DirectionEnum::Long && $isDownwardCross) {
151+
$this->exit($openPosition->quantity);
152+
} elseif ($openPosition->direction === DirectionEnum::Short && $isUpwardCross) {
153+
$this->exit($openPosition->quantity);
154+
}
155+
}
156+
}
157+
}
158+
}

0 commit comments

Comments
 (0)