Extending the engine
Analytics and the calculators in RoundlyConsulting\TradingAnalytics\Analytics\* are designed to be extended — subclass Analytics to register custom calculators and tell the facade to build it with TradingAnalytics::using() (see Custom calculators below), or replace the per-trade / after-trades hooks per instance:
$analytics = TradingAnalytics::for($trades)
->onEachTrade(function (Analytics $analytics, string $calculator, Trade $trade) {
// observe, skip or adjust the trade for this calculator, then run it
$calculator::calculatePerTrade($analytics, $trade);
})
->afterTrades(function (Analytics $analytics, string $calculator) {
$calculator::calculateAfterTrades($analytics);
})
->calculate();Hooks replace the default step
A hook is called once per calculator for every trade (onEachTrade) or once per calculator after the pass (afterTrades) — instead of the calculator’s own step. Call the step yourself to keep the default figures; leave it out and the metrics stay at zero. To observe and still calculate:
use RoundlyConsulting\TradingAnalytics\Analytics\Counts;
// Observe the trades one calculator sees, and keep every default figure
$seen = 0;
$analytics = TradingAnalytics::for($trades)
->onEachTrade(function (Analytics $analytics, string $calculator, Trade $trade) use (&$seen): void {
if ($calculator === Counts::class) {
$seen++; // observe
}
$calculator::calculatePerTrade($analytics, $trade); // keep the default step
})
->calculate();
$seen; // 3 — one call per trade for each calculatorCustom calculators
A calculator implements AnalyticsInterface: a static calculatePerTrade(Analytics, Trade), called for every trade, and a static calculateAfterTrades(Analytics), called once after the pass. Register it by subclassing Analytics and listing it in $defaultCalculators; declare any dependencies in $dependencies. Keep the constructor signature so make() and for() keep returning your subclass, then tell the facade to build it with TradingAnalytics::using() — once, e.g. in a service provider’s boot(), since the manager is a singleton:
use RoundlyConsulting\TradingAnalytics\Analytics;
use RoundlyConsulting\TradingAnalytics\DataTransferObjects\Trade;
use RoundlyConsulting\TradingAnalytics\Interfaces\AnalyticsInterface;
final class LongestHold implements AnalyticsInterface
{
public static function calculatePerTrade(Analytics $analytics, Trade $trade): void
{
if (! $analytics instanceof DeskAnalytics || $trade->isOpen()) {
return;
}
$seconds = (int) $trade->openTime->diffInSeconds($trade->closeTime);
$analytics->longestHoldSeconds = max($analytics->longestHoldSeconds, $seconds);
}
public static function calculateAfterTrades(Analytics $analytics): void
{
//
}
}
final class DeskAnalytics extends Analytics
{
public int $longestHoldSeconds = 0;
protected array $defaultCalculators = [
Analytics\Counts::class,
Analytics\RealizedGrossProfitAndLoss::class,
Analytics\RealizedNetProfitAndLoss::class,
Analytics\Wins::class,
LongestHold::class,
];
public function toArray(): array
{
return [
...parent::toArray(),
'longest_hold_seconds' => $this->longestHoldSeconds,
];
}
}
// Once, e.g. in a service provider's boot() — the manager is a singleton
TradingAnalytics::using(DeskAnalytics::class);
$desk = TradingAnalytics::calculate($trades); // a DeskAnalytics instance
$desk->longestHoldSeconds; // 86400
array_keys($desk->toArray());
// ['counts', 'wins', 'profit_and_loss', 'longest_hold_seconds']using() throws InvalidEngineException for a class that isn’t Analytics or a subclass of it. Custom calculators are accepted by only() and except() and listed by metrics().
- SequentialAnalyticsInterface — the marker for calculators whose figures follow the order trades close in, like MaxDrawdown, Streaks and the cumulative returns. While one of them runs, the engine refuses realized trades out of close-time order (see Trade order).
- MultiPassAnalyticsInterface — the marker RiskAdjustedReturns carries: it folds values in during the pass and derives its figures in calculateAfterTrades().
BcMath helpers
RoundlyConsulting\TradingAnalytics\Support\BcMath adds the roots bcmath lacks, computed without floats:
use RoundlyConsulting\TradingAnalytics\Support\BcMath;
BcMath::sqrt('2', 10); // '1.4142135623'
BcMath::nthRoot('27', 3, 10); // '3.0000000000' — Newton's method, no floatsShow your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.