Skip to content

Commit

Permalink
Removes brick/math, uses BCMath directly
Browse files Browse the repository at this point in the history
  • Loading branch information
jensscherbl committed Aug 9, 2024
1 parent dd5b45a commit 26cc057
Show file tree
Hide file tree
Showing 26 changed files with 131 additions and 274 deletions.
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Calculates [indicators][1] for [technical chart analysis][2] in [PHP][3].

- [PHP 8.3][4]
- Minimal dependencies.
- Uses [brick/math][5] for arbitrary precision numbers.
- Uses [BCMath][5] for arbitrary precision calculations.
- Avoids redundant calculations and keeps the overall complexity low.
- Unit- and integration tested against [other libraries][6] and [real-world data][7].

Expand Down Expand Up @@ -169,15 +169,15 @@ $result = $chart->getTrend($SMAPeriod, $EMAPeriod);

#### Why are numeric values represented as strings?

> Note about floating-point values: instantiating from a float might be unsafe, as floating-point values are imprecise by design, and could result in a loss of information. Always prefer instantiating from a string, which supports an unlimited number of digits.
> Floating point numbers have limited precision. [...] So never trust floating number results to the last digit, and do not compare floating point numbers directly for equality. If higher precision is necessary, the arbitrary precision math functions are available.
>
> [brick/math][5]
> [php.net][15]
[1]: https://en.wikipedia.org/wiki/Technical_indicator
[2]: https://en.wikipedia.org/wiki/Technical_analysis
[3]: https://www.php.net
[4]: https://www.php.net/releases/8.3/en.php
[5]: https://github.com/brick/math
[5]: https://www.php.net/manual/en/book.bc.php
[6]: https://github.com/bennycode/trading-signals
[7]: https://www.alphavantage.co
[8]: #prepare-chart
Expand All @@ -187,3 +187,4 @@ $result = $chart->getTrend($SMAPeriod, $EMAPeriod);
[12]: #ema-exponential-moving-average
[13]: #di---di-positive---negative-directional-indicator
[14]: #adx-average-directional-index
[15]: https://www.php.net/manual/en/language.types.float.php
3 changes: 1 addition & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,7 @@
"require":
{
"php": "^8.3",
"ext-bcmath": "*",
"brick/math": "^0.12"
"ext-bcmath": "*"
},
"require-dev":
{
Expand Down
19 changes: 9 additions & 10 deletions src/Candle/Candle.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@

namespace Kensho\Chart\Candle;

use Brick\Math\BigDecimal;
use Brick\Math\BigInteger;
use DomainException;
use Kensho\Chart\Number;

/**
* Open price, high price, low price, close price, volume,
Expand All @@ -13,14 +12,14 @@
final readonly class Candle
{
public function __construct(
public BigDecimal $open,
public BigDecimal $high,
public BigDecimal $low,
public BigDecimal $close,
public BigInteger $volume,
public BigDecimal $TR,
public BigDecimal $DMp,
public BigDecimal $DMm,
public Number $open,
public Number $high,
public Number $low,
public Number $close,
public Number $volume,
public Number $TR,
public Number $DMp,
public Number $DMm,
) {
if ($open->isNegative()) {
throw new DomainException(
Expand Down
37 changes: 19 additions & 18 deletions src/Candle/CandleFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,10 @@

namespace Kensho\Chart\Candle;

use Brick\Math\BigDecimal;
use Brick\Math\BigInteger;
use Brick\Math\Exception\MathException;
use Kensho\Chart\Number;

final readonly class CandleFactory implements CandleFactoryInterface
{
/**
* @throws MathException
*/
public static function create(
string $open,
string $high,
Expand All @@ -19,27 +14,33 @@ public static function create(
string $volume,
Candle|null $previous,
): Candle {
$open = BigDecimal::of($open);
$high = BigDecimal::of($high);
$low = BigDecimal::of($low);
$close = BigDecimal::of($close);
$volume = BigInteger::of($volume);
$open = new Number($open);
$high = new Number($high);
$low = new Number($low);
$close = new Number($close);
$volume = new Number($volume);
$highLow = $high->minus($low);
$TR = $highLow;
$DMp = BigDecimal::zero();
$DMm = BigDecimal::zero();
$DMp = new Number(0);
$DMm = new Number(0);

if ($previous !== null) {

/*
* Calculates true range (TR).
*/

$highClose = $high->minus($previous->close);
$absHighClose = $highClose->abs();
$lowClose = $low->minus($previous->close);
$absLowClose = $lowClose->abs();
$TR = BigDecimal::max($highLow, $absHighClose, $absLowClose);
$highClose = $high->minus($previous->close);
$absHighClose = $highClose->abs();
$lowClose = $low->minus($previous->close);
$absLowClose = $lowClose->abs();

if ($absHighClose->isGreaterThan($TR)) {
$TR = $absHighClose;
}
if ($absLowClose->isGreaterThan($TR)) {
$TR = $absLowClose;
}

/*
* Calculates directional movements (+DM & -DM).
Expand Down
51 changes: 12 additions & 39 deletions src/Chart/Chart.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@

namespace Kensho\Chart\Chart;

use Brick\Math\BigDecimal;
use Brick\Math\Exception\RoundingNecessaryException;
use Brick\Math\RoundingMode;
use Kensho\Chart\Candle\Candle;
use Kensho\Chart\DI;
use Kensho\Chart\Indicator\ADX\ADXFactoryInterface;
Expand All @@ -15,8 +12,7 @@

final readonly class Chart implements ChartInterface
{
private const ROUNDING_MODE = RoundingMode::HALF_UP;
private const SCALE = 4;
private const SCALE = 4;

/**
* @param array<string, Candle> $candles
Expand All @@ -29,9 +25,6 @@ public function __construct(
private ADXFactoryInterface $ADXFactory,
) {}

/**
* @throws RoundingNecessaryException
*/
public function getSMA(int $period): array
{
$SMA = $this->SMAFactory::create($period);
Expand All @@ -40,14 +33,11 @@ public function getSMA(int $period): array
foreach ($this->candles as $date => $candle) {
$close = $candle->close;
$SMAResult = $SMA->calculate($close);
$result[$date] = $this->round($SMAResult);
$result[$date] = $SMAResult?->round(self::SCALE);
}
return $result;
}

/**
* @throws RoundingNecessaryException
*/
public function getEMA(int $period): array
{
$EMA = $this->EMAFactory::create($period);
Expand All @@ -56,14 +46,11 @@ public function getEMA(int $period): array
foreach ($this->candles as $date => $candle) {
$close = $candle->close;
$EMAResult = $EMA->calculate($close);
$result[$date] = $this->round($EMAResult);
$result[$date] = $EMAResult?->round(self::SCALE);
}
return $result;
}

/**
* @throws RoundingNecessaryException
*/
public function getDI(int $period): array
{
$DI = $this->DIFactory::create($period);
Expand All @@ -75,17 +62,14 @@ public function getDI(int $period): array
$TR = $candle->TR;
$DIResult = $DI->calculate($DMp, $DMm, $TR);
$DIpResult = $DIResult->DIp;
$DIpRounded = $this->round($DIpResult);
$DIpRounded = $DIpResult?->round(self::SCALE);
$DImResult = $DIResult->DIm;
$DImRounded = $this->round($DImResult);
$DImRounded = $DImResult?->round(self::SCALE);
$result[$date] = new DI($DIpRounded, $DImRounded);
}
return $result;
}

/**
* @throws RoundingNecessaryException
*/
public function getADX(int $period): array
{
$DI = $this->DIFactory::create($period);
Expand All @@ -102,17 +86,14 @@ public function getADX(int $period): array

if ($DIpResult !== null && $DImResult !== null) {
$ADXResult = $ADX->calculate($DIpResult, $DImResult);
$result[$date] = $this->round($ADXResult);
$result[$date] = $ADXResult?->round(self::SCALE);
} else {
$result[$date] = null;
}
}
return $result;
}

/**
* @throws RoundingNecessaryException
*/
public function getTrend(int $SMAPeriod, int $EMAPeriod): array
{
$SMA = $this->SMAFactory::create($SMAPeriod);
Expand All @@ -123,24 +104,24 @@ public function getTrend(int $SMAPeriod, int $EMAPeriod): array

foreach ($this->candles as $date => $candle) {
$close = $candle->close;
$closeRounded = $this->round($close);
$closeRounded = $close->round(self::SCALE);
$SMAResult = $SMA->calculate($close);
$SMARounded = $this->round($SMAResult);
$SMARounded = $SMAResult?->round(self::SCALE);
$EMAResult = $EMA->calculate($close);
$EMARounded = $this->round($EMAResult);
$EMARounded = $EMAResult?->round(self::SCALE);
$DMp = $candle->DMp;
$DMm = $candle->DMm;
$TR = $candle->TR;
$DIResult = $DI->calculate($DMp, $DMm, $TR);
$DIpResult = $DIResult->DIp;
$DIpRounded = $this->round($DIpResult);
$DIpRounded = $DIpResult?->round(self::SCALE);
$DImResult = $DIResult->DIm;
$DImRounded = $this->round($DImResult);
$DImRounded = $DImResult?->round(self::SCALE);
$ADXRounded = null;

if ($DIpResult !== null && $DImResult !== null) {
$ADXResult = $ADX->calculate($DIpResult, $DImResult);
$ADXRounded = $this->round($ADXResult);
$ADXRounded = $ADXResult?->round(self::SCALE);
}

$result[$date] = new Trend(
Expand All @@ -154,12 +135,4 @@ public function getTrend(int $SMAPeriod, int $EMAPeriod): array
}
return $result;
}

/**
* @throws RoundingNecessaryException
*/
private function round(BigDecimal|null $value): string|null
{
return $value?->toScale(self::SCALE, self::ROUNDING_MODE)->__toString();
}
}
15 changes: 4 additions & 11 deletions src/Indicator/ADX/ADX.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,16 @@

namespace Kensho\Chart\Indicator\ADX;

use Brick\Math\BigDecimal;
use Brick\Math\Exception\MathException;
use Kensho\Chart\Indicator\PrecisionTrait;
use Kensho\Chart\Indicator\WSMA\WSMAInterface;
use Kensho\Chart\Number;

final readonly class ADX implements ADXInterface
{
use PrecisionTrait;

public function __construct(
private WSMAInterface $WSMA,
) {}

/**
* @throws MathException
*/
public function calculate(BigDecimal $DIp, BigDecimal $DIm): BigDecimal|null
public function calculate(Number $DIp, Number $DIm): Number|null
{
/*
* Calculates the directional movement index (DI).
Expand All @@ -28,9 +21,9 @@ public function calculate(BigDecimal $DIp, BigDecimal $DIm): BigDecimal|null
$denominator = $DIp->plus($DIm);

if ($denominator->isZero()) {
$DX = BigDecimal::zero();
$DX = new Number(0);
} else {
$DX = $numerator->dividedBy($denominator, self::SCALE, self::ROUNDING_MODE)->multipliedBy(100);
$DX = $numerator->dividedBy($denominator)->multipliedBy(100);
}

/*
Expand Down
4 changes: 2 additions & 2 deletions src/Indicator/ADX/ADXInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

namespace Kensho\Chart\Indicator\ADX;

use Brick\Math\BigDecimal;
use Kensho\Chart\Number;

/**
* Calculates the average directional movement index (ADX).
*/
interface ADXInterface
{
public function calculate(BigDecimal $DIp, BigDecimal $DIm): BigDecimal|null;
public function calculate(Number $DIp, Number $DIm): Number|null;
}
19 changes: 6 additions & 13 deletions src/Indicator/DI/DI.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,18 @@

namespace Kensho\Chart\Indicator\DI;

use Brick\Math\BigDecimal;
use Brick\Math\Exception\MathException;
use Kensho\Chart\Indicator\PrecisionTrait;
use Kensho\Chart\Indicator\WSMA\WSMAInterface;
use Kensho\Chart\Number;

final readonly class DI implements DIInterface
{
use PrecisionTrait;

public function __construct(
private WSMAInterface $DMpSMA,
private WSMAInterface $DMmSMA,
private WSMAInterface $ATR,
) {}

/**
* @throws MathException
*/
public function calculate(BigDecimal $DMp, BigDecimal $DMm, BigDecimal $TR): DIResult
public function calculate(Number $DMp, Number $DMm, Number $TR): DIResult
{
/*
* Calculates the smoothed moving averages
Expand All @@ -45,11 +38,11 @@ public function calculate(BigDecimal $DMp, BigDecimal $DMm, BigDecimal $TR): DIR
*/

if ($ATR->isZero()) {
$DIp = BigDecimal::zero();
$DIm = BigDecimal::zero();
$DIp = new Number(0);
$DIm = new Number(0);
} else {
$DIp = $DMpSMA->dividedBy($ATR, self::SCALE, self::ROUNDING_MODE)->multipliedBy(100);
$DIm = $DMmSMA->dividedBy($ATR, self::SCALE, self::ROUNDING_MODE)->multipliedBy(100);
$DIp = $DMpSMA->dividedBy($ATR)->multipliedBy(100);
$DIm = $DMmSMA->dividedBy($ATR)->multipliedBy(100);
}
}
return new DIResult(
Expand Down
4 changes: 2 additions & 2 deletions src/Indicator/DI/DIInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

namespace Kensho\Chart\Indicator\DI;

use Brick\Math\BigDecimal;
use Kensho\Chart\Number;

/**
* Calculates the directional indicators (+DI & -DI).
*/
interface DIInterface
{
public function calculate(BigDecimal $DMp, BigDecimal $DMm, BigDecimal $TR): DIResult;
public function calculate(Number $DMp, Number $DMm, Number $TR): DIResult;
}
Loading

0 comments on commit 26cc057

Please sign in to comment.