| @@ -6,7 +6,9 @@ declare(strict_types=1); | |||
| namespace App\Arbitration\Inner; | |||
| use App\Exchanges\ExchangeInterface; | |||
| use App\Exchanges\MarketInterface; | |||
| use App\Utils\Money\Currency; | |||
| class ArbitrationChain | |||
| { | |||
| @@ -15,10 +17,16 @@ class ArbitrationChain | |||
| * @var MarketInterface[] | |||
| */ | |||
| private array $markets; | |||
| private ExchangeInterface $exchange; | |||
| private ?Currency $base = null; | |||
| public function __construct(MarketInterface ...$markets) | |||
| public function __construct( | |||
| ExchangeInterface $exchange, | |||
| MarketInterface ...$markets | |||
| ) | |||
| { | |||
| $this->markets = $markets; | |||
| $this->exchange = $exchange; | |||
| } | |||
| /** | |||
| @@ -28,4 +36,19 @@ class ArbitrationChain | |||
| { | |||
| return $this->markets; | |||
| } | |||
| public function getExchange(): ExchangeInterface | |||
| { | |||
| return $this->exchange; | |||
| } | |||
| public function getBase(): ?Currency | |||
| { | |||
| return $this->base; | |||
| } | |||
| public function setBase(?Currency $base): void | |||
| { | |||
| $this->base = $base; | |||
| } | |||
| } | |||
| @@ -16,7 +16,7 @@ interface ChainProviderInterface | |||
| */ | |||
| public function getChainsForExchange(ExchangeInterface $exchange): array; | |||
| public function addChain(ExchangeInterface $exchange, ArbitrationChain $chain): void; | |||
| public function addChain(ArbitrationChain $chain): void; | |||
| public function removeChain(ExchangeInterface $exchange, ArbitrationChain $chain): void; | |||
| public function removeChain(ArbitrationChain $chain): void; | |||
| } | |||
| @@ -0,0 +1,43 @@ | |||
| <?php | |||
| declare(strict_types=1); | |||
| namespace App\Arbitration\Inner\Receipt; | |||
| use App\Exchanges\MarketInterface; | |||
| use App\Utils\Money\Money; | |||
| class Deal | |||
| { | |||
| private MarketInterface $market; | |||
| private Money $bid; | |||
| private Money $ask; | |||
| public function __construct( | |||
| MarketInterface $market, | |||
| Money $bid, | |||
| Money $ask | |||
| ) | |||
| { | |||
| $this->market = $market; | |||
| $this->bid = $bid; | |||
| $this->ask = $ask; | |||
| } | |||
| public function getBid(): Money | |||
| { | |||
| return $this->bid; | |||
| } | |||
| public function getAsk(): Money | |||
| { | |||
| return $this->ask; | |||
| } | |||
| public function getMarket(): MarketInterface | |||
| { | |||
| return $this->market; | |||
| } | |||
| } | |||
| @@ -0,0 +1,28 @@ | |||
| <?php | |||
| declare(strict_types=1); | |||
| namespace App\Arbitration\Inner\Receipt; | |||
| class Receipt | |||
| { | |||
| /** | |||
| * @var Deal[] | |||
| */ | |||
| private array $deals; | |||
| public function __construct(Deal ...$deals) | |||
| { | |||
| $this->deals = $deals; | |||
| } | |||
| /** | |||
| * @return Deal[] | |||
| */ | |||
| public function getDeals() | |||
| { | |||
| return $this->deals; | |||
| } | |||
| } | |||
| @@ -0,0 +1,223 @@ | |||
| <?php | |||
| declare(strict_types=1); | |||
| namespace App\Arbitration\Inner\ReceiptCalculus; | |||
| use App\Arbitration\Inner\ArbitrationChain; | |||
| use App\Arbitration\Inner\Receipt\Deal; | |||
| use App\Arbitration\Inner\Receipt\Receipt; | |||
| use App\Exchanges\MarketInterface; | |||
| use App\Utils\Money\Currency; | |||
| use App\Utils\Money\Money; | |||
| use App\Utils\Order\Order; | |||
| use App\Utils\Wallet\WalletInterface; | |||
| class Calculator | |||
| { | |||
| private WalletInterface $wallet; | |||
| public function __construct(WalletInterface $wallet) | |||
| { | |||
| $this->wallet = $wallet; | |||
| } | |||
| public function calculateArbitrationReceipt(ArbitrationChain $chain): ?Receipt | |||
| { | |||
| $base = $chain->getBase(); | |||
| if (!$base) { | |||
| return null; | |||
| } | |||
| $markets = $chain->getMarkets(); | |||
| $baseMarkets = array_filter( | |||
| $markets, | |||
| function (MarketInterface $market) use($base): bool | |||
| { | |||
| $pair = $market->getPair(); | |||
| return $pair->getBase()->isEqual($base) || $pair->getQuote()->isEqual($base); | |||
| } | |||
| ); | |||
| foreach ($baseMarkets as $baseMarket) { | |||
| usort( | |||
| $markets, | |||
| function (MarketInterface $a, MarketInterface $b) use($baseMarket) { | |||
| if ($a === $baseMarket) { | |||
| return 1; | |||
| } | |||
| if ($b === $baseMarket) { | |||
| return -1; | |||
| } | |||
| return 0; | |||
| } | |||
| ); | |||
| $orders = $this->getOrdersAgainstBase($markets, $base); | |||
| $receipt = $this->build($orders, $base); | |||
| if ($receipt) { | |||
| return $receipt; | |||
| } | |||
| } | |||
| return null; | |||
| } | |||
| /** | |||
| * @param MarketInterface[] $markets | |||
| * @param Currency $base | |||
| * @return array | |||
| */ | |||
| public function getOrdersAgainstBase(array $markets, Currency $base): array | |||
| { | |||
| $current = $base; | |||
| $result = []; | |||
| while (\count($result) < \count($markets)) { | |||
| foreach ($markets as $market) { | |||
| $pair = $market->getPair(); | |||
| if ($pair->getBase()->isEqual($current)) { | |||
| $orders = $market->getOrderBook()->getBids(); | |||
| $current = $pair->getQuote(); | |||
| } elseif ($pair->getQuote()->isEqual($current)) { | |||
| $orders = $market->getOrderBook()->getBids(); | |||
| $current = $pair->getBase(); | |||
| } else { | |||
| continue; | |||
| } | |||
| $result[] = [ | |||
| 'market' => $market, | |||
| 'orders' => $orders, | |||
| 'buy_currency' => $current | |||
| ]; | |||
| break; | |||
| } | |||
| } | |||
| return $result; | |||
| } | |||
| protected function build(array $ordersData, Currency $base): ?Receipt | |||
| { | |||
| $available = $this->wallet->getMoneyAmount($base); | |||
| $receiptDeals = []; | |||
| foreach ($ordersData as $orderData) { | |||
| $buyAmount = $this->calculateAmount( | |||
| $ordersData['orders'], | |||
| $ordersData['buy_currency'], | |||
| $available | |||
| ); | |||
| if (null === $available) { | |||
| return null; | |||
| } | |||
| /** | |||
| * @var MarketInterface $market | |||
| */ | |||
| $market = $ordersData['market']; | |||
| $receiptDeals[] = new Deal( | |||
| $market, | |||
| $available, | |||
| $buyAmount | |||
| ); | |||
| $available = new Money( | |||
| bcmul( | |||
| $buyAmount->getAmount(), | |||
| (string)(1 - $market->getFee()), | |||
| $buyAmount->getCurrency()->getPrecision() | |||
| ), | |||
| $buyAmount->getCurrency() | |||
| ); | |||
| } | |||
| /** | |||
| * @var Deal $last | |||
| */ | |||
| $last = end($receiptDeals); | |||
| if ($this->wallet->getMoneyAmount($base)->compare($last->getAsk()) <= 0) { | |||
| return null; | |||
| } | |||
| return new Receipt(...$receiptDeals); | |||
| } | |||
| protected function calculateAmount(array $orders, Currency $buy, Money $maxAmount): ?Money | |||
| { | |||
| usort( | |||
| $orders, | |||
| function (Order $a, Order $b): int | |||
| { | |||
| return bccomp( | |||
| bcdiv( | |||
| $a->getFrom()->getAmount(), | |||
| $a->getTo()->getAmount(), | |||
| max($a->getFrom()->getCurrency()->getPrecision(), $a->getTo()->getCurrency()->getPrecision()) | |||
| ), | |||
| bcdiv( | |||
| $b->getFrom()->getAmount(), | |||
| $b->getTo()->getAmount(), | |||
| max($b->getFrom()->getCurrency()->getPrecision(), $b->getTo()->getCurrency()->getPrecision()) | |||
| ), | |||
| max( | |||
| $a->getFrom()->getCurrency()->getPrecision(), | |||
| $a->getTo()->getCurrency()->getPrecision(), | |||
| $b->getFrom()->getCurrency()->getPrecision(), | |||
| $b->getTo()->getCurrency()->getPrecision(), | |||
| ) | |||
| ); | |||
| } | |||
| ); | |||
| $currency = new Money("0", $buy); | |||
| $precision = max($maxAmount->getCurrency()->getPrecision(), $buy->getPrecision()); | |||
| $highestPrice = null; | |||
| foreach ($orders as $order) { | |||
| /** | |||
| * @var Order $order | |||
| */ | |||
| $currency->add($order->getTo()); | |||
| $highestPrice = new Money( | |||
| bcdiv( | |||
| $order->getFrom()->getAmount(), | |||
| $order->getTo()->getAmount(), | |||
| $precision | |||
| ), | |||
| $precision | |||
| ); | |||
| $total = new Money( | |||
| bcmul( | |||
| $currency->getAmount(), | |||
| $highestPrice->getAmount(), | |||
| $precision | |||
| ), | |||
| $order->getFrom()->getCurrency() | |||
| ); | |||
| if ($total->compare($maxAmount) <= 0) { | |||
| break; | |||
| } | |||
| } | |||
| if (null === $highestPrice) { | |||
| return null; | |||
| } | |||
| return new Money( | |||
| bcdiv( | |||
| $maxAmount->getAmount(), | |||
| $highestPrice->getAmount(), | |||
| $precision | |||
| ), | |||
| $orders[0]['buy_currency'] | |||
| ); | |||
| } | |||
| } | |||
| @@ -48,7 +48,12 @@ class ArbitrationMarketChainsFinder | |||
| return $result; | |||
| } | |||
| protected function cycleToChain(array $cycle, array $rows, array $markets): ArbitrationChain | |||
| protected function cycleToChain( | |||
| ExchangeInterface $exchange, | |||
| array $cycle, | |||
| array $rows, | |||
| array $markets | |||
| ): ArbitrationChain | |||
| { | |||
| $cycleMarkets = []; | |||
| for($i = 0; $i < \count($cycle); $i++) { | |||
| @@ -80,7 +85,7 @@ class ArbitrationMarketChainsFinder | |||
| } | |||
| } | |||
| return new ArbitrationChain(...$cycleMarkets); | |||
| return new ArbitrationChain($exchange, ...$cycleMarkets); | |||
| } | |||
| /** | |||
| @@ -30,4 +30,18 @@ class ExchangeProvider | |||
| { | |||
| return iterator_to_array($this->exchanges->getIterator()); | |||
| } | |||
| public function getExchangeByName(string $name): ?ExchangeInterface | |||
| { | |||
| foreach ($this->exchanges as $exchange) { | |||
| /** | |||
| * @var ExchangeInterface $exchange | |||
| */ | |||
| if ($exchange->getName() === $name) { | |||
| return $exchange; | |||
| } | |||
| } | |||
| return null; | |||
| } | |||
| } | |||
| @@ -58,10 +58,18 @@ class KunaMarket implements MarketInterface | |||
| $bids = []; | |||
| foreach ($this->apiExecutor->getBook($this->id) as $orderData) { | |||
| $order = new Order( | |||
| new Money((string)$orderData[0], $this->currencyPair->getQuote()), | |||
| new Money((string)abs($orderData[1]), $this->currencyPair->getBase()), | |||
| ); | |||
| if ($orderData[1] > 0) { | |||
| $order = new Order( | |||
| new Money((string)$orderData[0], $this->currencyPair->getQuote()), | |||
| new Money((string)abs($orderData[1]), $this->currencyPair->getBase()), | |||
| ); | |||
| } else { | |||
| $order = new Order( | |||
| new Money((string)abs($orderData[1]), $this->currencyPair->getBase()), | |||
| new Money((string)$orderData[0], $this->currencyPair->getQuote()), | |||
| ); | |||
| } | |||
| $ordersCount = $orderData[2]; | |||
| while ($ordersCount) { | |||
| @@ -76,4 +84,9 @@ class KunaMarket implements MarketInterface | |||
| return new OrderBook($bids, $asks); | |||
| } | |||
| public function getFee(): float | |||
| { | |||
| return 0.0025; | |||
| } | |||
| } | |||
| @@ -7,10 +7,13 @@ namespace App\Exchanges; | |||
| use App\Utils\Money\CurrencyPair; | |||
| use App\Utils\Order\Book\OrderBook; | |||
| interface MarketInterface | |||
| { | |||
| public function getPair(): CurrencyPair; | |||
| public function getOrderBook(): OrderBook; | |||
| public function getPair(): CurrencyPair; | |||
| public function getFee(): float; | |||
| } | |||
| @@ -9,7 +9,9 @@ namespace App\Infrastructure\Arbitration\Inner; | |||
| use App\Arbitration\Inner\ArbitrationChain; | |||
| use App\Arbitration\Inner\ChainProviderInterface; | |||
| use App\Exchanges\ExchangeInterface; | |||
| use App\Exchanges\ExchangeProvider; | |||
| use App\Exchanges\MarketInterface; | |||
| use App\Utils\Money\Currency; | |||
| use MongoDB\Client; | |||
| use MongoDB\Collection; | |||
| @@ -45,18 +47,27 @@ class ChainProvider implements ChainProviderInterface | |||
| } | |||
| } | |||
| $base = null; | |||
| if (!empty($row['base'])) { | |||
| $base = new Currency($row['base']['code'], $row['base']['precision']); | |||
| } | |||
| if (!empty($chainMarkets)) { | |||
| $chains[] = new ArbitrationChain(...$chainMarkets); | |||
| $chain = new ArbitrationChain($exchange, ...$chainMarkets); | |||
| if ($base) { | |||
| $chain->setBase($base); | |||
| } | |||
| $chains[] = $chain; | |||
| } | |||
| } | |||
| return $chains; | |||
| } | |||
| protected function calculateChainHash(ExchangeInterface $exchange, ArbitrationChain $chain): string | |||
| protected function calculateChainHash(ArbitrationChain $chain): string | |||
| { | |||
| return md5( | |||
| $exchange->getName() | |||
| $chain->getExchange()->getName() | |||
| . ';' | |||
| . implode( | |||
| ';', | |||
| @@ -71,29 +82,34 @@ class ChainProvider implements ChainProviderInterface | |||
| ); | |||
| } | |||
| public function addChain(ExchangeInterface $exchange, ArbitrationChain $chain): void | |||
| public function addChain(ArbitrationChain $chain): void | |||
| { | |||
| $base = $chain->getBase(); | |||
| $this | |||
| ->collection | |||
| ->insertOne([ | |||
| '_id' => $this->calculateChainHash($exchange, $chain), | |||
| 'exchange' => $exchange->getName(), | |||
| 'chain' => array_map( | |||
| static function (MarketInterface $market): string | |||
| { | |||
| return (string)$market->getPair(); | |||
| }, | |||
| $chain->getMarkets() | |||
| ) | |||
| ]); | |||
| ->updateOne( | |||
| ['_id' => $this->calculateChainHash($chain)], | |||
| [ | |||
| 'exchange' => $chain->getExchange()->getName(), | |||
| 'chain' => array_map( | |||
| static function (MarketInterface $market): string | |||
| { | |||
| return (string)$market->getPair(); | |||
| }, | |||
| $chain->getMarkets() | |||
| ), | |||
| 'base' => $base ? ['code' => $base->getCode(), 'precision' => $base->getPrecision()] : null | |||
| ], | |||
| ['upsert' => true] | |||
| ); | |||
| } | |||
| public function removeChain(ExchangeInterface $exchange, ArbitrationChain $chain): void | |||
| public function removeChain(ArbitrationChain $chain): void | |||
| { | |||
| $this | |||
| ->collection | |||
| ->deleteOne([ | |||
| '_id' => $this->calculateChainHash($exchange, $chain) | |||
| '_id' => $this->calculateChainHash($chain) | |||
| ]); | |||
| } | |||
| } | |||
| @@ -33,6 +33,11 @@ class Currency | |||
| return $this->code; | |||
| } | |||
| public function getPrecision(): int | |||
| { | |||
| return $this->precision; | |||
| } | |||
| public function __toString() | |||
| { | |||
| return $this->code; | |||
| @@ -36,4 +36,31 @@ class Money | |||
| { | |||
| return $this->amount; | |||
| } | |||
| public function compare(Money $money): int | |||
| { | |||
| if (!$this->currency->isEqual($money->getCurrency())) { | |||
| throw new \InvalidArgumentException('Different currencies cannot be compared'); | |||
| } | |||
| return bccomp($this->amount, $money->getAmount(), $this->currency->getPrecision()); | |||
| } | |||
| public function add(Money $money): Money | |||
| { | |||
| if (!$this->currency->isEqual($money->getCurrency())) { | |||
| throw new \InvalidArgumentException('Different currencies cannot added'); | |||
| } | |||
| return new Money(bcadd($this->amount, $money->amount, $this->currency->getPrecision()), $this->currency); | |||
| } | |||
| public function sub(Money $money): Money | |||
| { | |||
| if (!$this->currency->isEqual($money->getCurrency())) { | |||
| throw new \InvalidArgumentException('Different currencies cannot added'); | |||
| } | |||
| return new Money(bcsub($this->amount, $money->amount, $this->currency->getPrecision()), $this->currency); | |||
| } | |||
| } | |||
| @@ -0,0 +1,32 @@ | |||
| <?php | |||
| declare(strict_types=1); | |||
| namespace App\Utils\Wallet; | |||
| use App\Utils\Money\Currency; | |||
| use App\Utils\Money\Money; | |||
| class Wallet implements WalletInterface | |||
| { | |||
| private array $monies; | |||
| public function __construct(Money ...$monies) | |||
| { | |||
| $this->monies = $monies; | |||
| } | |||
| public function getMoneyAmount(Currency $currency): Money | |||
| { | |||
| foreach ($this->monies as $money) { | |||
| if ($money->getCurrency()->isEqual($currency)) { | |||
| return $money; | |||
| } | |||
| } | |||
| return new Money("0", $currency); | |||
| } | |||
| } | |||
| @@ -0,0 +1,16 @@ | |||
| <?php | |||
| declare(strict_types=1); | |||
| namespace App\Utils\Wallet; | |||
| use App\Utils\Money\Currency; | |||
| use App\Utils\Money\Money; | |||
| interface WalletInterface | |||
| { | |||
| public function getMoneyAmount(Currency $currency): Money; | |||
| } | |||