Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit a8bf402

Browse files
feat: add detailed backtest performance results (#1)
1 parent e1e09a4 commit a8bf402

File tree

5 files changed

+100
-20
lines changed

5 files changed

+100
-20
lines changed

‎src/Command/BacktestingCommand.php

Lines changed: 65 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
namespace Stochastix\Command;
44

5+
use Psr\EventDispatcher\EventDispatcherInterface;
56
use Stochastix\Domain\Backtesting\Dto\BacktestConfiguration;
7+
use Stochastix\Domain\Backtesting\Event\BacktestPhaseEvent;
68
use Stochastix\Domain\Backtesting\Repository\BacktestResultRepositoryInterface;
79
use Stochastix\Domain\Backtesting\Service\Backtester;
810
use Stochastix\Domain\Backtesting\Service\BacktestResultSaver;
@@ -16,7 +18,6 @@
1618
use Symfony\Component\Console\Output\OutputInterface;
1719
use Symfony\Component\Console\Style\SymfonyStyle;
1820
use Symfony\Component\Stopwatch\Stopwatch;
19-
use Symfony\Component\Stopwatch\StopwatchEvent;
2021

2122
#[AsCommand(
2223
name: 'stochastix:backtesting',
@@ -34,7 +35,8 @@ public function __construct(
3435
private readonly Backtester $backtester,
3536
private readonly ConfigurationResolver $configResolver,
3637
private readonly BacktestResultRepositoryInterface $resultRepository,
37-
private readonly BacktestResultSaver $resultSaver
38+
private readonly BacktestResultSaver $resultSaver,
39+
private readonly EventDispatcherInterface $eventDispatcher,
3840
) {
3941
parent::__construct();
4042
}
@@ -61,21 +63,38 @@ protected function execute(InputInterface $input, OutputInterface $output): int
6163
$strategyAlias = $input->getArgument('strategy-alias');
6264

6365
$stopwatch = new Stopwatch(true);
64-
$stopwatch->start('backtest_execute');
66+
$runId = null;
6567

66-
$io->title(sprintf('🚀 Stochastix Backtester Initializing: %s 🚀', $strategyAlias));
68+
$listener = function (BacktestPhaseEvent $event) use ($stopwatch, &$runId) {
69+
if ($event->runId !== $runId) {
70+
return;
71+
}
72+
73+
$phaseName = $event->phase;
74+
75+
if ($event->eventType === 'start' && !$stopwatch->isStarted($phaseName)) {
76+
$stopwatch->start($phaseName);
77+
} elseif ($event->eventType === 'stop' && $stopwatch->isStarted($phaseName)) {
78+
$stopwatch->stop($phaseName);
79+
}
80+
};
81+
82+
$this->eventDispatcher->addListener(BacktestPhaseEvent::class, $listener);
6783

6884
try {
85+
$io->title(sprintf('🚀 Stochastix Backtester Initializing: %s 🚀', $strategyAlias));
86+
87+
$stopwatch->start('configuration');
6988
$io->text('Resolving configuration...');
7089
$config = $this->configResolver->resolve($input);
7190
$io->text('Configuration resolved.');
7291
$io->newLine();
92+
$stopwatch->stop('configuration');
7393

7494
if ($savePath = $input->getOption('save-config')) {
7595
$this->saveConfigToJson($config, $savePath);
7696
$io->success("Configuration saved to {$savePath}. Exiting as requested.");
77-
$event = $stopwatch->stop('backtest_execute');
78-
$this->displayExecutionTime($io, $event);
97+
$this->displayExecutionTime($io, $stopwatch);
7998

8099
return Command::SUCCESS;
81100
}
@@ -104,27 +123,28 @@ protected function execute(InputInterface $input, OutputInterface $output): int
104123
$io->definitionList(...$definitions);
105124

106125
$io->section('Starting Backtest Run...');
107-
$results = $this->backtester->run($config);
108126
$runId = $this->resultRepository->generateRunId($config->strategyAlias);
109127
$io->note("Generated Run ID: {$runId}");
110128

129+
$results = $this->backtester->run($config, $runId);
130+
131+
$stopwatch->start('saving');
111132
$this->resultSaver->save($runId, $results);
133+
$stopwatch->stop('saving');
112134

113135
$io->section('Backtest Performance Summary');
114136
$this->displaySummaryStats($io, $results);
115137
$this->displayTradesLog($io, $results['closedTrades']);
116138
$this->displayOpenPositionsLog($io, $results['openPositions'] ?? []); // NEW
117139

118140
$io->newLine();
119-
$event = $stopwatch->stop('backtest_execute');
120-
$this->displayExecutionTime($io, $event);
141+
$this->displayExecutionTime($io, $stopwatch);
121142
$io->newLine();
122143
$io->success(sprintf('Backtest for "%s" finished successfully.', $strategyAlias));
123144

124145
return Command::SUCCESS;
125146
} catch (\Exception $e) {
126-
$event = $stopwatch->stop('backtest_execute');
127-
$this->displayExecutionTime($io, $event, true);
147+
$this->displayExecutionTime($io, $stopwatch, true);
128148

129149
$io->error([
130150
'💥 An error occurred:',
@@ -137,17 +157,47 @@ protected function execute(InputInterface $input, OutputInterface $output): int
137157
}
138158

139159
return Command::FAILURE;
160+
} finally {
161+
$this->eventDispatcher->removeListener(BacktestPhaseEvent::class, $listener);
140162
}
141163
}
142164

143-
private function displayExecutionTime(SymfonyStyle $io, StopwatchEvent$event, bool $errorOccurred = false): void
165+
private function displayExecutionTime(SymfonyStyle $io, Stopwatch$stopwatch, bool $errorOccurred = false): void
144166
{
167+
$rows = [];
168+
$totalDuration = 0;
169+
$peakMemory = 0;
170+
171+
$phases = ['configuration', 'initialization', 'loop', 'statistics', 'saving'];
172+
173+
foreach ($phases as $phase) {
174+
if ($stopwatch->isStarted($phase)) {
175+
$stopwatch->stop($phase);
176+
}
177+
178+
try {
179+
$event = $stopwatch->getEvent($phase);
180+
$duration = $event->getDuration();
181+
$memory = $event->getMemory();
182+
$totalDuration += $duration;
183+
$peakMemory = max($peakMemory, $memory);
184+
185+
$rows[] = [ucfirst($phase), sprintf('%.2f ms', $duration), sprintf('%.2f MB', $memory / (1024 ** 2))];
186+
} catch (\LogicException) {
187+
// Event was not started/stopped, so we can't display it
188+
continue;
189+
}
190+
}
191+
192+
$io->section('Execution Profile');
193+
$io->table(['Phase', 'Duration', 'Memory'], $rows);
194+
145195
$messagePrefix = $errorOccurred ? '📊 Backtest ran for' : '📊 Backtest finished in';
146196
$io->writeln(sprintf(
147-
'%s: <info>%.2f ms</info> / Memory usage: <info>%.2f MB</info>',
197+
'%s: <info>%.2f ms</info> / Peak Memory usage: <info>%.2f MB</info>',
148198
$messagePrefix,
149-
$event->getDuration(),
150-
$event->getMemory() / (1024 ** 2)
199+
$totalDuration,
200+
$peakMemory / (1024 ** 2)
151201
));
152202
}
153203

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace Stochastix\Domain\Backtesting\Event;
4+
5+
namespace Stochastix\Domain\Backtesting\Event;
6+
7+
use Symfony\Contracts\EventDispatcher\Event;
8+
9+
final class BacktestPhaseEvent extends Event
10+
{
11+
public function __construct(
12+
public readonly string $runId,
13+
public readonly string $phase,
14+
public readonly string $eventType,
15+
) {
16+
}
17+
}

‎src/Domain/Backtesting/MessageHandler/RunBacktestMessageHandler.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public function __invoke(RunBacktestMessage $message): void
5050
}
5151
};
5252

53-
$results = $this->backtester->run($message->configuration, $progressCallback);
53+
$results = $this->backtester->run($message->configuration, $runId, $progressCallback);
5454

5555
$this->resultSaver->save($runId, $results);
5656

‎src/Domain/Backtesting/Service/Backtester.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44

55
use Ds\Map;
66
use Ds\Vector;
7+
use Psr\EventDispatcher\EventDispatcherInterface;
78
use Psr\Log\LoggerInterface;
89
use Stochastix\Domain\Backtesting\Dto\BacktestConfiguration;
10+
use Stochastix\Domain\Backtesting\Event\BacktestPhaseEvent;
911
use Stochastix\Domain\Backtesting\Model\BacktestCursor;
1012
use Stochastix\Domain\Common\Enum\DirectionEnum;
1113
use Stochastix\Domain\Common\Enum\OhlcvEnum;
@@ -33,14 +35,16 @@ public function __construct(
3335
private StatisticsServiceInterface $statisticsService,
3436
private SeriesMetricServiceInterface $seriesMetricService,
3537
private MultiTimeframeDataServiceInterface $multiTimeframeDataService,
38+
private EventDispatcherInterface $eventDispatcher,
3639
private LoggerInterface $logger,
3740
#[Autowire('%kernel.project_dir%/data/market')]
3841
private string $baseDataPath,
3942
) {
4043
}
4144

42-
public function run(BacktestConfiguration $config, ?callable $progressCallback = null): array
45+
public function run(BacktestConfiguration $config, string$runId, ?callable $progressCallback = null): array
4346
{
47+
$this->eventDispatcher->dispatch(new BacktestPhaseEvent($runId, 'initialization', 'start'));
4448
$this->logger->info('Starting backtest run for strategy: {strategy}', ['strategy' => $config->strategyAlias]);
4549

4650
$portfolioManager = new PortfolioManager($this->logger);
@@ -87,7 +91,9 @@ public function run(BacktestConfiguration $config, ?callable $progressCallback =
8791
$indicatorDataForSave = [];
8892
$allTimestamps = [];
8993
$lastBars = null;
94+
$this->eventDispatcher->dispatch(new BacktestPhaseEvent($runId, 'initialization', 'stop'));
9095

96+
$this->eventDispatcher->dispatch(new BacktestPhaseEvent($runId, 'loop', 'start'));
9197
foreach ($config->symbols as $symbol) {
9298
$this->logger->info('--- Starting backtest for Symbol: {symbol} ---', ['symbol' => $symbol]);
9399
$strategy = $this->strategyRegistry->getStrategy($config->strategyAlias);
@@ -203,7 +209,9 @@ public function run(BacktestConfiguration $config, ?callable $progressCallback =
203209

204210
$this->logger->info('--- Finished backtest for Symbol: {symbol} ---', ['symbol' => $symbol]);
205211
}
212+
$this->eventDispatcher->dispatch(new BacktestPhaseEvent($runId, 'loop', 'stop'));
206213

214+
$this->eventDispatcher->dispatch(new BacktestPhaseEvent($runId, 'statistics', 'start'));
207215
$this->logger->info('All symbols processed.');
208216

209217
// 1. Sum P&L from all closed trades
@@ -271,6 +279,7 @@ public function run(BacktestConfiguration $config, ?callable $progressCallback =
271279
$results['timeSeriesMetrics'] = $this->seriesMetricService->calculate($results);
272280
$this->logger->info('Time-series metrics calculated.');
273281
unset($results['marketData']);
282+
$this->eventDispatcher->dispatch(new BacktestPhaseEvent($runId, 'statistics', 'stop'));
274283

275284
return $results;
276285
}

‎tests/Domain/Backtesting/Service/BacktesterTest.php

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use org\bovigo\vfs\vfsStream;
66
use org\bovigo\vfs\vfsStreamDirectory;
77
use PHPUnit\Framework\TestCase;
8+
use Psr\EventDispatcher\EventDispatcherInterface;
89
use Psr\Log\NullLogger;
910
use Stochastix\Domain\Backtesting\Dto\BacktestConfiguration;
1011
use Stochastix\Domain\Backtesting\Service\Backtester;
@@ -29,6 +30,7 @@ class BacktesterTest extends TestCase
2930
private StatisticsServiceInterface $statisticsServiceMock;
3031
private SeriesMetricServiceInterface $seriesMetricServiceMock;
3132
private MultiTimeframeDataServiceInterface $multiTimeframeDataServiceMock;
33+
private EventDispatcherInterface $eventDispatcherMock;
3234
private vfsStreamDirectory $vfsRoot;
3335

3436
protected function setUp(): void
@@ -40,6 +42,7 @@ protected function setUp(): void
4042
$this->statisticsServiceMock = $this->createMock(StatisticsServiceInterface::class);
4143
$this->seriesMetricServiceMock = $this->createMock(SeriesMetricServiceInterface::class);
4244
$this->multiTimeframeDataServiceMock = $this->createMock(MultiTimeframeDataServiceInterface::class);
45+
$this->eventDispatcherMock = $this->createMock(EventDispatcherInterface::class);
4346

4447
$this->vfsRoot = vfsStream::setup('data');
4548

@@ -49,6 +52,7 @@ protected function setUp(): void
4952
$this->statisticsServiceMock,
5053
$this->seriesMetricServiceMock,
5154
$this->multiTimeframeDataServiceMock,
55+
$this->eventDispatcherMock,
5256
new NullLogger(),
5357
$this->vfsRoot->url()
5458
);
@@ -100,7 +104,7 @@ public function testRunExecutesFullLifecycleForSingleSymbol(): void
100104
$this->statisticsServiceMock->expects($this->once())->method('calculate')->willReturn(['summaryMetrics' => ['finalBalance' => '10000']]);
101105
$this->seriesMetricServiceMock->expects($this->once())->method('calculate')->willReturn(['equity' => ['value' => [10000, 10000]]]);
102106

103-
$results = $this->backtester->run($config);
107+
$results = $this->backtester->run($config, 'test_run');
104108

105109
$this->assertIsArray($results);
106110
$this->assertArrayHasKey('status', $results);
@@ -152,7 +156,7 @@ public function testProgressCallbackIsInvokedCorrectly(): void
152156
$this->assertEquals($callCount, $processed);
153157
};
154158

155-
$this->backtester->run($config, $progressCallback);
159+
$this->backtester->run($config, 'test_run', $progressCallback);
156160

157161
$this->assertEquals(5, $callCount);
158162
}
@@ -201,7 +205,7 @@ public function testRunHandlesUnclosedShortPositionCorrectly(): void
201205
$this->statisticsServiceMock->method('calculate')->willReturn([]);
202206
$this->seriesMetricServiceMock->method('calculate')->willReturn([]);
203207

204-
$results = $this->backtester->run($config);
208+
$results = $this->backtester->run($config, 'test_run');
205209

206210
// Unrealized PNL = (Entry Price - Current Price) * Quantity = (3100 - 2900) * 0.5 = 100
207211
$expectedUnrealizedPnl = '100';

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /