| @@ -6,7 +6,9 @@ declare(strict_types=1); | |||||
| namespace App\Arbitration\Inner; | namespace App\Arbitration\Inner; | ||||
| use App\Exchanges\ExchangeInterface; | |||||
| use App\Exchanges\MarketInterface; | use App\Exchanges\MarketInterface; | ||||
| use App\Utils\Money\Currency; | |||||
| class ArbitrationChain | class ArbitrationChain | ||||
| { | { | ||||
| @@ -15,10 +17,16 @@ class ArbitrationChain | |||||
| * @var MarketInterface[] | * @var MarketInterface[] | ||||
| */ | */ | ||||
| private array $markets; | 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->markets = $markets; | ||||
| $this->exchange = $exchange; | |||||
| } | } | ||||
| /** | /** | ||||
| @@ -28,4 +36,19 @@ class ArbitrationChain | |||||
| { | { | ||||
| return $this->markets; | 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 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; | 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 = []; | $cycleMarkets = []; | ||||
| for($i = 0; $i < \count($cycle); $i++) { | 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()); | 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 = []; | $bids = []; | ||||
| foreach ($this->apiExecutor->getBook($this->id) as $orderData) { | 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]; | $ordersCount = $orderData[2]; | ||||
| while ($ordersCount) { | while ($ordersCount) { | ||||
| @@ -76,4 +84,9 @@ class KunaMarket implements MarketInterface | |||||
| return new OrderBook($bids, $asks); | 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\Money\CurrencyPair; | ||||
| use App\Utils\Order\Book\OrderBook; | |||||
| interface MarketInterface | 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\ArbitrationChain; | ||||
| use App\Arbitration\Inner\ChainProviderInterface; | use App\Arbitration\Inner\ChainProviderInterface; | ||||
| use App\Exchanges\ExchangeInterface; | use App\Exchanges\ExchangeInterface; | ||||
| use App\Exchanges\ExchangeProvider; | |||||
| use App\Exchanges\MarketInterface; | use App\Exchanges\MarketInterface; | ||||
| use App\Utils\Money\Currency; | |||||
| use MongoDB\Client; | use MongoDB\Client; | ||||
| use MongoDB\Collection; | 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)) { | if (!empty($chainMarkets)) { | ||||
| $chains[] = new ArbitrationChain(...$chainMarkets); | |||||
| $chain = new ArbitrationChain($exchange, ...$chainMarkets); | |||||
| if ($base) { | |||||
| $chain->setBase($base); | |||||
| } | |||||
| $chains[] = $chain; | |||||
| } | } | ||||
| } | } | ||||
| return $chains; | return $chains; | ||||
| } | } | ||||
| protected function calculateChainHash(ExchangeInterface $exchange, ArbitrationChain $chain): string | |||||
| protected function calculateChainHash(ArbitrationChain $chain): string | |||||
| { | { | ||||
| return md5( | return md5( | ||||
| $exchange->getName() | |||||
| $chain->getExchange()->getName() | |||||
| . ';' | . ';' | ||||
| . implode( | . 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 | $this | ||||
| ->collection | ->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 | $this | ||||
| ->collection | ->collection | ||||
| ->deleteOne([ | ->deleteOne([ | ||||
| '_id' => $this->calculateChainHash($exchange, $chain) | |||||
| '_id' => $this->calculateChainHash($chain) | |||||
| ]); | ]); | ||||
| } | } | ||||
| } | } | ||||
| @@ -33,6 +33,11 @@ class Currency | |||||
| return $this->code; | return $this->code; | ||||
| } | } | ||||
| public function getPrecision(): int | |||||
| { | |||||
| return $this->precision; | |||||
| } | |||||
| public function __toString() | public function __toString() | ||||
| { | { | ||||
| return $this->code; | return $this->code; | ||||
| @@ -36,4 +36,31 @@ class Money | |||||
| { | { | ||||
| return $this->amount; | 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; | |||||
| } | |||||